chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveCopyParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
newName?: string
|
||||
destinationFolderId?: string
|
||||
}
|
||||
|
||||
interface GoogleDriveCopyResponse extends ToolResponse {
|
||||
output: {
|
||||
file: GoogleDriveFile
|
||||
}
|
||||
}
|
||||
|
||||
export const copyTool: ToolConfig<GoogleDriveCopyParams, GoogleDriveCopyResponse> = {
|
||||
id: 'google_drive_copy',
|
||||
name: 'Copy Google Drive File',
|
||||
description: 'Create a copy of a file in Google Drive',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to copy',
|
||||
},
|
||||
newName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name for the copied file (defaults to "Copy of [original name]")',
|
||||
},
|
||||
destinationFolderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the folder to place the copy in (defaults to same location as original)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/copy`)
|
||||
url.searchParams.append('fields', ALL_FILE_FIELDS)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.newName) {
|
||||
body.name = params.newName
|
||||
}
|
||||
if (params.destinationFolderId) {
|
||||
body.parents = [params.destinationFolderId.trim()]
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json().catch(() => ({}) as any)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.error?.message ||
|
||||
`Failed to copy Google Drive file (${response.status} ${response.statusText})`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'The copied file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID of the copy' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
createdTime: { type: 'string', description: 'File creation time' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
owners: { type: 'json', description: 'List of file owners' },
|
||||
size: { type: 'string', description: 'File size in bytes' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import type { GoogleDriveComment, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_COMMENT_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveCreateCommentParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
content: string
|
||||
anchor?: string
|
||||
}
|
||||
|
||||
interface GoogleDriveCreateCommentResponse extends ToolResponse {
|
||||
output: {
|
||||
comment: GoogleDriveComment
|
||||
}
|
||||
}
|
||||
|
||||
export const createCommentTool: ToolConfig<
|
||||
GoogleDriveCreateCommentParams,
|
||||
GoogleDriveCreateCommentResponse
|
||||
> = {
|
||||
id: 'google_drive_create_comment',
|
||||
name: 'Create Google Drive Comment',
|
||||
description: 'Add a comment to a file in Google Drive',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to comment on',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The plain text content of the comment',
|
||||
},
|
||||
anchor: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'A region of the document the comment refers to (JSON anchor string)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/comments`
|
||||
)
|
||||
url.searchParams.append('fields', ALL_COMMENT_FIELDS)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
content: params.content,
|
||||
}
|
||||
if (params.anchor) {
|
||||
body.anchor = params.anchor
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = (await response.json()) as GoogleDriveComment
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
comment: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
comment: {
|
||||
type: 'json',
|
||||
description: 'The created comment',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Comment ID' },
|
||||
content: { type: 'string', description: 'Plain text content of the comment' },
|
||||
htmlContent: { type: 'string', description: 'HTML-formatted content of the comment' },
|
||||
author: { type: 'json', description: 'User who authored the comment' },
|
||||
createdTime: { type: 'string', description: 'When the comment was created' },
|
||||
modifiedTime: { type: 'string', description: 'When the comment was last modified' },
|
||||
resolved: { type: 'boolean', description: 'Whether the comment has been resolved' },
|
||||
deleted: { type: 'boolean', description: 'Whether the comment has been deleted' },
|
||||
anchor: { type: 'string', description: 'Region of the document the comment refers to' },
|
||||
quotedFileContent: { type: 'json', description: 'The file content the comment quotes' },
|
||||
replies: { type: 'json', description: 'Threaded replies to the comment' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { GoogleDriveToolParams, GoogleDriveUploadResponse } from '@/tools/google_drive/types'
|
||||
import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleDriveCreateFolderTool')
|
||||
|
||||
export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResponse> = {
|
||||
id: 'google_drive_create_folder',
|
||||
name: 'Create Folder in Google Drive',
|
||||
description: 'Create a new folder in Google Drive with complete metadata returned',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Drive API',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the folder to create',
|
||||
},
|
||||
folderSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Drive parent folder ID to create the folder in (e.g., 1ABCxyz...)',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'ID of the parent folder (internal use)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true',
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const metadata: {
|
||||
name: string | undefined
|
||||
mimeType: string
|
||||
parents?: string[]
|
||||
} = {
|
||||
name: params.fileName,
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
}
|
||||
|
||||
// Add parent folder if specified (prefer folderSelector over folderId)
|
||||
const parentFolderId = (params.folderSelector || params.folderId)?.trim()
|
||||
if (parentFolderId) {
|
||||
metadata.parents = [parentFolderId.trim()]
|
||||
}
|
||||
|
||||
return metadata
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: GoogleDriveToolParams) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to create folder in Google Drive', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: data,
|
||||
})
|
||||
throw new Error(data.error?.message || 'Failed to create folder in Google Drive')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const folderId = data.id
|
||||
const authHeader = `Bearer ${params?.accessToken || ''}`
|
||||
|
||||
// Fetch complete folder metadata with all fields
|
||||
const metadataResponse = await fetch(
|
||||
`https://www.googleapis.com/drive/v3/files/${folderId}?supportsAllDrives=true&fields=${ALL_FILE_FIELDS}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!metadataResponse.ok) {
|
||||
logger.warn('Failed to fetch complete metadata, returning basic response', {
|
||||
status: metadataResponse.status,
|
||||
statusText: metadataResponse.statusText,
|
||||
})
|
||||
// Return basic response if metadata fetch fails
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const fullMetadata = await metadataResponse.json()
|
||||
|
||||
logger.info('Folder created successfully', {
|
||||
folderId: fullMetadata.id,
|
||||
name: fullMetadata.name,
|
||||
mimeType: fullMetadata.mimeType,
|
||||
hasOwners: !!fullMetadata.owners?.length,
|
||||
hasPermissions: !!fullMetadata.permissions?.length,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: fullMetadata,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'object',
|
||||
description: 'Complete created folder metadata from Google Drive',
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive folder ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'Folder name' },
|
||||
mimeType: { type: 'string', description: 'MIME type (application/vnd.google-apps.folder)' },
|
||||
description: { type: 'string', description: 'Folder description' },
|
||||
// Ownership & Sharing
|
||||
owners: { type: 'json', description: 'List of folder owners' },
|
||||
permissions: { type: 'json', description: 'Folder permissions' },
|
||||
permissionIds: { type: 'json', description: 'Permission IDs' },
|
||||
shared: { type: 'boolean', description: 'Whether folder is shared' },
|
||||
ownedByMe: { type: 'boolean', description: 'Whether owned by current user' },
|
||||
writersCanShare: { type: 'boolean', description: 'Whether writers can share' },
|
||||
viewersCanCopyContent: { type: 'boolean', description: 'Whether viewers can copy' },
|
||||
copyRequiresWriterPermission: {
|
||||
type: 'boolean',
|
||||
description: 'Whether copy requires writer permission',
|
||||
},
|
||||
sharingUser: { type: 'json', description: 'User who shared the folder' },
|
||||
// Labels/Tags
|
||||
starred: { type: 'boolean', description: 'Whether folder is starred' },
|
||||
trashed: { type: 'boolean', description: 'Whether folder is in trash' },
|
||||
explicitlyTrashed: { type: 'boolean', description: 'Whether explicitly trashed' },
|
||||
properties: { type: 'json', description: 'Custom properties' },
|
||||
appProperties: { type: 'json', description: 'App-specific properties' },
|
||||
folderColorRgb: { type: 'string', description: 'Folder color' },
|
||||
// Timestamps
|
||||
createdTime: { type: 'string', description: 'Folder creation time' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
modifiedByMeTime: { type: 'string', description: 'When modified by current user' },
|
||||
viewedByMeTime: { type: 'string', description: 'When last viewed by current user' },
|
||||
sharedWithMeTime: { type: 'string', description: 'When shared with current user' },
|
||||
// User Info
|
||||
lastModifyingUser: { type: 'json', description: 'User who last modified the folder' },
|
||||
viewedByMe: { type: 'boolean', description: 'Whether viewed by current user' },
|
||||
modifiedByMe: { type: 'boolean', description: 'Whether modified by current user' },
|
||||
// Links
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
iconLink: { type: 'string', description: 'URL to folder icon' },
|
||||
// Hierarchy & Location
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
spaces: { type: 'json', description: 'Spaces containing folder' },
|
||||
driveId: { type: 'string', description: 'Shared drive ID' },
|
||||
// Capabilities
|
||||
capabilities: { type: 'json', description: 'User capabilities on folder' },
|
||||
// Versions
|
||||
version: { type: 'string', description: 'Version number' },
|
||||
// Other
|
||||
isAppAuthorized: { type: 'boolean', description: 'Whether created by requesting app' },
|
||||
contentRestrictions: { type: 'json', description: 'Content restrictions' },
|
||||
linkShareMetadata: { type: 'json', description: 'Link share metadata' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveDeleteParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
}
|
||||
|
||||
interface GoogleDriveDeleteResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
fileId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteTool: ToolConfig<GoogleDriveDeleteParams, GoogleDriveDeleteResponse> = {
|
||||
id: 'google_drive_delete',
|
||||
name: 'Delete Google Drive File',
|
||||
description: 'Permanently delete a file from Google Drive (bypasses trash)',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to permanently delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}) as any)
|
||||
throw new Error(
|
||||
data.error?.message ||
|
||||
`Failed to delete Google Drive file (${response.status} ${response.statusText})`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
fileId: params?.fileId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the file was successfully deleted',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the deleted file',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveDeleteCommentParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
commentId: string
|
||||
}
|
||||
|
||||
interface GoogleDriveDeleteCommentResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
fileId: string
|
||||
commentId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteCommentTool: ToolConfig<
|
||||
GoogleDriveDeleteCommentParams,
|
||||
GoogleDriveDeleteCommentResponse
|
||||
> = {
|
||||
id: 'google_drive_delete_comment',
|
||||
name: 'Delete Google Drive Comment',
|
||||
description: 'Delete a comment from a file in Google Drive',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file the comment belongs to',
|
||||
},
|
||||
commentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the comment to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/comments/${params.commentId?.trim()}`,
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (_response: Response, params) => ({
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
fileId: params?.fileId ?? '',
|
||||
commentId: params?.commentId ?? '',
|
||||
},
|
||||
}),
|
||||
|
||||
outputs: {
|
||||
deleted: { type: 'boolean', description: 'Whether the comment was successfully deleted' },
|
||||
fileId: { type: 'string', description: 'The ID of the file' },
|
||||
commentId: { type: 'string', description: 'The ID of the deleted comment' },
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import type { GoogleDriveDownloadResponse, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const downloadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveDownloadResponse> = {
|
||||
id: 'google_drive_download',
|
||||
name: 'Download File from Google Drive',
|
||||
description:
|
||||
'Download a file from Google Drive with complete metadata (exports Google Workspace files automatically)',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Drive API',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to download',
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The MIME type to export Google Workspace files to (optional)',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional filename override',
|
||||
},
|
||||
includeRevisions: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Whether to include revision history in the metadata (default: true, returns first 100 revisions)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/google_drive/download',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessToken: params.accessToken,
|
||||
fileId: params.fileId,
|
||||
mimeType: params.mimeType,
|
||||
fileName: params.fileName,
|
||||
includeRevisions: params.includeRevisions,
|
||||
}),
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'file',
|
||||
description: 'Downloaded file stored in execution files',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Complete file metadata from Google Drive',
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
description: { type: 'string', description: 'File description' },
|
||||
originalFilename: { type: 'string', description: 'Original uploaded filename' },
|
||||
fullFileExtension: { type: 'string', description: 'Full file extension' },
|
||||
fileExtension: { type: 'string', description: 'File extension' },
|
||||
// Ownership & Sharing
|
||||
owners: { type: 'json', description: 'List of file owners' },
|
||||
permissions: { type: 'json', description: 'File permissions' },
|
||||
permissionIds: { type: 'json', description: 'Permission IDs' },
|
||||
shared: { type: 'boolean', description: 'Whether file is shared' },
|
||||
ownedByMe: { type: 'boolean', description: 'Whether owned by current user' },
|
||||
writersCanShare: { type: 'boolean', description: 'Whether writers can share' },
|
||||
viewersCanCopyContent: { type: 'boolean', description: 'Whether viewers can copy' },
|
||||
copyRequiresWriterPermission: {
|
||||
type: 'boolean',
|
||||
description: 'Whether copy requires writer permission',
|
||||
},
|
||||
sharingUser: { type: 'json', description: 'User who shared the file' },
|
||||
// Labels/Tags
|
||||
starred: { type: 'boolean', description: 'Whether file is starred' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash' },
|
||||
explicitlyTrashed: { type: 'boolean', description: 'Whether explicitly trashed' },
|
||||
properties: { type: 'json', description: 'Custom properties' },
|
||||
appProperties: { type: 'json', description: 'App-specific properties' },
|
||||
// Timestamps
|
||||
createdTime: { type: 'string', description: 'File creation time' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
modifiedByMeTime: { type: 'string', description: 'When modified by current user' },
|
||||
viewedByMeTime: { type: 'string', description: 'When last viewed by current user' },
|
||||
sharedWithMeTime: { type: 'string', description: 'When shared with current user' },
|
||||
// User Info
|
||||
lastModifyingUser: { type: 'json', description: 'User who last modified the file' },
|
||||
viewedByMe: { type: 'boolean', description: 'Whether viewed by current user' },
|
||||
modifiedByMe: { type: 'boolean', description: 'Whether modified by current user' },
|
||||
// Links
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
webContentLink: { type: 'string', description: 'Direct download URL' },
|
||||
iconLink: { type: 'string', description: 'URL to file icon' },
|
||||
thumbnailLink: { type: 'string', description: 'URL to thumbnail' },
|
||||
exportLinks: { type: 'json', description: 'Export format links' },
|
||||
// Size & Storage
|
||||
size: { type: 'string', description: 'File size in bytes' },
|
||||
quotaBytesUsed: { type: 'string', description: 'Storage quota used' },
|
||||
// Checksums
|
||||
md5Checksum: { type: 'string', description: 'MD5 hash' },
|
||||
sha1Checksum: { type: 'string', description: 'SHA-1 hash' },
|
||||
sha256Checksum: { type: 'string', description: 'SHA-256 hash' },
|
||||
// Hierarchy & Location
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
spaces: { type: 'json', description: 'Spaces containing file' },
|
||||
driveId: { type: 'string', description: 'Shared drive ID' },
|
||||
// Capabilities
|
||||
capabilities: { type: 'json', description: 'User capabilities on file' },
|
||||
// Versions
|
||||
version: { type: 'string', description: 'Version number' },
|
||||
headRevisionId: { type: 'string', description: 'Head revision ID' },
|
||||
// Media Metadata
|
||||
hasThumbnail: { type: 'boolean', description: 'Whether has thumbnail' },
|
||||
thumbnailVersion: { type: 'string', description: 'Thumbnail version' },
|
||||
imageMediaMetadata: { type: 'json', description: 'Image-specific metadata' },
|
||||
videoMediaMetadata: { type: 'json', description: 'Video-specific metadata' },
|
||||
// Other
|
||||
isAppAuthorized: { type: 'boolean', description: 'Whether created by requesting app' },
|
||||
contentRestrictions: { type: 'json', description: 'Content restrictions' },
|
||||
linkShareMetadata: { type: 'json', description: 'Link share metadata' },
|
||||
// Revisions
|
||||
revisions: {
|
||||
type: 'json',
|
||||
description: 'File revision history (first 100 revisions only)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveExportParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
mimeType: string
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
interface GoogleDriveExportResponse extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}
|
||||
exportedMimeType: string
|
||||
}
|
||||
}
|
||||
|
||||
export const exportTool: ToolConfig<GoogleDriveExportParams, GoogleDriveExportResponse> = {
|
||||
id: 'google_drive_export',
|
||||
name: 'Export Google Drive File',
|
||||
description:
|
||||
'Export a Google Workspace file (Docs, Sheets, Slides, Drawings) to a chosen format such as PDF, DOCX, XLSX, or CSV',
|
||||
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 Drive API',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the Google Workspace file to export',
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The target MIME type to export to (e.g. application/pdf, text/csv)',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional filename override for the exported file',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: '/api/tools/google_drive/export',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({
|
||||
accessToken: params.accessToken,
|
||||
fileId: params.fileId,
|
||||
mimeType: params.mimeType,
|
||||
fileName: params.fileName,
|
||||
}),
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'file',
|
||||
description: 'Exported file stored in execution files',
|
||||
},
|
||||
exportedMimeType: {
|
||||
type: 'string',
|
||||
description: 'The MIME type the file was exported to',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { GoogleDriveToolParams, GoogleDriveUser } from '@/tools/google_drive/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveGetAboutParams extends GoogleDriveToolParams {}
|
||||
|
||||
interface GoogleDriveGetAboutResponse extends ToolResponse {
|
||||
output: {
|
||||
user: GoogleDriveUser & {
|
||||
emailAddress: string
|
||||
}
|
||||
storageQuota: {
|
||||
limit: string | null
|
||||
usage: string
|
||||
usageInDrive: string
|
||||
usageInDriveTrash: string
|
||||
}
|
||||
canCreateDrives: boolean
|
||||
importFormats: Record<string, string[]>
|
||||
exportFormats: Record<string, string[]>
|
||||
maxUploadSize: string
|
||||
}
|
||||
}
|
||||
|
||||
export const getAboutTool: ToolConfig<GoogleDriveGetAboutParams, GoogleDriveGetAboutResponse> = {
|
||||
id: 'google_drive_get_about',
|
||||
name: 'Get Google Drive Info',
|
||||
description:
|
||||
'Get information about the user and their Google Drive (storage quota, capabilities)',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: () => {
|
||||
const url = new URL('https://www.googleapis.com/drive/v3/about')
|
||||
url.searchParams.append(
|
||||
'fields',
|
||||
'user,storageQuota,canCreateDrives,importFormats,exportFormats,maxUploadSize'
|
||||
)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to get Google Drive info')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
user: {
|
||||
displayName: data.user?.displayName ?? null,
|
||||
emailAddress: data.user?.emailAddress ?? '',
|
||||
photoLink: data.user?.photoLink ?? null,
|
||||
permissionId: data.user?.permissionId ?? null,
|
||||
me: data.user?.me ?? true,
|
||||
},
|
||||
storageQuota: {
|
||||
limit: data.storageQuota?.limit ?? null,
|
||||
usage: data.storageQuota?.usage ?? '0',
|
||||
usageInDrive: data.storageQuota?.usageInDrive ?? '0',
|
||||
usageInDriveTrash: data.storageQuota?.usageInDriveTrash ?? '0',
|
||||
},
|
||||
canCreateDrives: data.canCreateDrives ?? false,
|
||||
importFormats: data.importFormats ?? {},
|
||||
exportFormats: data.exportFormats ?? {},
|
||||
maxUploadSize: data.maxUploadSize ?? '0',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
user: {
|
||||
type: 'json',
|
||||
description: 'Information about the authenticated user',
|
||||
properties: {
|
||||
displayName: { type: 'string', description: 'User display name' },
|
||||
emailAddress: { type: 'string', description: 'User email address' },
|
||||
photoLink: { type: 'string', description: 'URL to user profile photo', optional: true },
|
||||
permissionId: { type: 'string', description: 'User permission ID' },
|
||||
me: { type: 'boolean', description: 'Whether this is the authenticated user' },
|
||||
},
|
||||
},
|
||||
storageQuota: {
|
||||
type: 'json',
|
||||
description: 'Storage quota information in bytes',
|
||||
properties: {
|
||||
limit: {
|
||||
type: 'string',
|
||||
description: 'Total storage limit in bytes (null for unlimited)',
|
||||
optional: true,
|
||||
},
|
||||
usage: { type: 'string', description: 'Total storage used in bytes' },
|
||||
usageInDrive: { type: 'string', description: 'Storage used by Drive files in bytes' },
|
||||
usageInDriveTrash: {
|
||||
type: 'string',
|
||||
description: 'Storage used by trashed files in bytes',
|
||||
},
|
||||
},
|
||||
},
|
||||
canCreateDrives: {
|
||||
type: 'boolean',
|
||||
description: 'Whether user can create shared drives',
|
||||
},
|
||||
importFormats: {
|
||||
type: 'json',
|
||||
description: 'Map of MIME types that can be imported and their target formats',
|
||||
},
|
||||
exportFormats: {
|
||||
type: 'json',
|
||||
description: 'Map of Google Workspace MIME types and their exportable formats',
|
||||
},
|
||||
maxUploadSize: {
|
||||
type: 'string',
|
||||
description: 'Maximum upload size in bytes',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type {
|
||||
GoogleDriveFile,
|
||||
GoogleDriveGetContentResponse,
|
||||
GoogleDriveRevision,
|
||||
GoogleDriveToolParams,
|
||||
} from '@/tools/google_drive/types'
|
||||
import {
|
||||
ALL_FILE_FIELDS,
|
||||
ALL_REVISION_FIELDS,
|
||||
DEFAULT_EXPORT_FORMATS,
|
||||
GOOGLE_WORKSPACE_MIME_TYPES,
|
||||
} from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleDriveGetContentTool')
|
||||
|
||||
export const getContentTool: ToolConfig<GoogleDriveToolParams, GoogleDriveGetContentResponse> = {
|
||||
id: 'google_drive_get_content',
|
||||
name: 'Get Content from Google Drive',
|
||||
description:
|
||||
'Get content from a file in Google Drive with complete metadata (exports Google Workspace files automatically)',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Drive API',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to get content from',
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The MIME type to export Google Workspace files to (optional)',
|
||||
},
|
||||
includeRevisions: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Whether to include revision history in the metadata (default: true, returns first 100 revisions)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}?fields=${ALL_FILE_FIELDS}&supportsAllDrives=true`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
transformResponse: async (response: Response, params?: GoogleDriveToolParams) => {
|
||||
try {
|
||||
if (!response.ok) {
|
||||
const errorDetails = await response.json().catch(() => ({}))
|
||||
logger.error('Failed to get file metadata', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
error: errorDetails,
|
||||
})
|
||||
throw new Error(errorDetails.error?.message || 'Failed to get file metadata')
|
||||
}
|
||||
|
||||
const metadata: GoogleDriveFile = await response.json()
|
||||
const fileId = metadata.id
|
||||
const mimeType = metadata.mimeType
|
||||
const authHeader = `Bearer ${params?.accessToken || ''}`
|
||||
|
||||
let content: string
|
||||
|
||||
if (GOOGLE_WORKSPACE_MIME_TYPES.includes(mimeType)) {
|
||||
const exportFormat = params?.mimeType || DEFAULT_EXPORT_FORMATS[mimeType] || 'text/plain'
|
||||
logger.info('Exporting Google Workspace file', {
|
||||
fileId,
|
||||
mimeType,
|
||||
exportFormat,
|
||||
})
|
||||
|
||||
const exportResponse = await fetch(
|
||||
`https://www.googleapis.com/drive/v3/files/${fileId}/export?mimeType=${encodeURIComponent(exportFormat)}&supportsAllDrives=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!exportResponse.ok) {
|
||||
const exportError = await exportResponse.json().catch(() => ({}))
|
||||
logger.error('Failed to export file', {
|
||||
status: exportResponse.status,
|
||||
statusText: exportResponse.statusText,
|
||||
error: exportError,
|
||||
})
|
||||
throw new Error(exportError.error?.message || 'Failed to export Google Workspace file')
|
||||
}
|
||||
|
||||
content = await exportResponse.text()
|
||||
} else {
|
||||
logger.info('Downloading regular file', {
|
||||
fileId,
|
||||
mimeType,
|
||||
})
|
||||
|
||||
const downloadResponse = await fetch(
|
||||
`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&supportsAllDrives=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!downloadResponse.ok) {
|
||||
const downloadError = await downloadResponse.json().catch(() => ({}))
|
||||
logger.error('Failed to download file', {
|
||||
status: downloadResponse.status,
|
||||
statusText: downloadResponse.statusText,
|
||||
error: downloadError,
|
||||
})
|
||||
throw new Error(downloadError.error?.message || 'Failed to download file')
|
||||
}
|
||||
|
||||
content = await downloadResponse.text()
|
||||
}
|
||||
|
||||
const includeRevisions = params?.includeRevisions !== false
|
||||
const canReadRevisions = metadata.capabilities?.canReadRevisions === true
|
||||
if (includeRevisions && canReadRevisions) {
|
||||
try {
|
||||
const revisionsResponse = await fetch(
|
||||
`https://www.googleapis.com/drive/v3/files/${fileId}/revisions?fields=revisions(${ALL_REVISION_FIELDS})&pageSize=100`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (revisionsResponse.ok) {
|
||||
const revisionsData = await revisionsResponse.json()
|
||||
metadata.revisions = revisionsData.revisions as GoogleDriveRevision[]
|
||||
logger.info('Fetched file revisions', {
|
||||
fileId,
|
||||
revisionCount: metadata.revisions?.length || 0,
|
||||
})
|
||||
} else {
|
||||
logger.warn('Failed to fetch revisions, continuing without them', {
|
||||
status: revisionsResponse.status,
|
||||
statusText: revisionsResponse.statusText,
|
||||
})
|
||||
}
|
||||
} catch (revisionError: any) {
|
||||
logger.warn('Error fetching revisions, continuing without them', {
|
||||
error: revisionError.message,
|
||||
})
|
||||
}
|
||||
} else if (includeRevisions && !canReadRevisions) {
|
||||
logger.info('Skipping revision fetch - user does not have canReadRevisions permission', {
|
||||
fileId,
|
||||
})
|
||||
}
|
||||
|
||||
logger.info('File content retrieved successfully', {
|
||||
fileId,
|
||||
name: metadata.name,
|
||||
mimeType: metadata.mimeType,
|
||||
contentLength: content.length,
|
||||
hasOwners: !!metadata.owners?.length,
|
||||
hasPermissions: !!metadata.permissions?.length,
|
||||
hasRevisions: !!metadata.revisions?.length,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content,
|
||||
metadata,
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Error in transform response', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'File content as text (Google Workspace files are exported)',
|
||||
},
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Complete file metadata from Google Drive',
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
description: { type: 'string', description: 'File description' },
|
||||
originalFilename: { type: 'string', description: 'Original uploaded filename' },
|
||||
fullFileExtension: { type: 'string', description: 'Full file extension' },
|
||||
fileExtension: { type: 'string', description: 'File extension' },
|
||||
// Ownership & Sharing
|
||||
owners: { type: 'json', description: 'List of file owners' },
|
||||
permissions: { type: 'json', description: 'File permissions' },
|
||||
permissionIds: { type: 'json', description: 'Permission IDs' },
|
||||
shared: { type: 'boolean', description: 'Whether file is shared' },
|
||||
ownedByMe: { type: 'boolean', description: 'Whether owned by current user' },
|
||||
writersCanShare: { type: 'boolean', description: 'Whether writers can share' },
|
||||
viewersCanCopyContent: { type: 'boolean', description: 'Whether viewers can copy' },
|
||||
copyRequiresWriterPermission: {
|
||||
type: 'boolean',
|
||||
description: 'Whether copy requires writer permission',
|
||||
},
|
||||
sharingUser: { type: 'json', description: 'User who shared the file' },
|
||||
// Labels/Tags
|
||||
starred: { type: 'boolean', description: 'Whether file is starred' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash' },
|
||||
explicitlyTrashed: { type: 'boolean', description: 'Whether explicitly trashed' },
|
||||
properties: { type: 'json', description: 'Custom properties' },
|
||||
appProperties: { type: 'json', description: 'App-specific properties' },
|
||||
// Timestamps
|
||||
createdTime: { type: 'string', description: 'File creation time' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
modifiedByMeTime: { type: 'string', description: 'When modified by current user' },
|
||||
viewedByMeTime: { type: 'string', description: 'When last viewed by current user' },
|
||||
sharedWithMeTime: { type: 'string', description: 'When shared with current user' },
|
||||
// User Info
|
||||
lastModifyingUser: { type: 'json', description: 'User who last modified the file' },
|
||||
viewedByMe: { type: 'boolean', description: 'Whether viewed by current user' },
|
||||
modifiedByMe: { type: 'boolean', description: 'Whether modified by current user' },
|
||||
// Links
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
webContentLink: { type: 'string', description: 'Direct download URL' },
|
||||
iconLink: { type: 'string', description: 'URL to file icon' },
|
||||
thumbnailLink: { type: 'string', description: 'URL to thumbnail' },
|
||||
exportLinks: { type: 'json', description: 'Export format links' },
|
||||
// Size & Storage
|
||||
size: { type: 'string', description: 'File size in bytes' },
|
||||
quotaBytesUsed: { type: 'string', description: 'Storage quota used' },
|
||||
// Checksums
|
||||
md5Checksum: { type: 'string', description: 'MD5 hash' },
|
||||
sha1Checksum: { type: 'string', description: 'SHA-1 hash' },
|
||||
sha256Checksum: { type: 'string', description: 'SHA-256 hash' },
|
||||
// Hierarchy & Location
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
spaces: { type: 'json', description: 'Spaces containing file' },
|
||||
driveId: { type: 'string', description: 'Shared drive ID' },
|
||||
// Capabilities
|
||||
capabilities: { type: 'json', description: 'User capabilities on file' },
|
||||
// Versions
|
||||
version: { type: 'string', description: 'Version number' },
|
||||
headRevisionId: { type: 'string', description: 'Head revision ID' },
|
||||
// Media Metadata
|
||||
hasThumbnail: { type: 'boolean', description: 'Whether has thumbnail' },
|
||||
thumbnailVersion: { type: 'string', description: 'Thumbnail version' },
|
||||
imageMediaMetadata: { type: 'json', description: 'Image-specific metadata' },
|
||||
videoMediaMetadata: { type: 'json', description: 'Video-specific metadata' },
|
||||
// Other
|
||||
isAppAuthorized: { type: 'boolean', description: 'Whether created by requesting app' },
|
||||
contentRestrictions: { type: 'json', description: 'Content restrictions' },
|
||||
linkShareMetadata: { type: 'json', description: 'Link share metadata' },
|
||||
// Revisions
|
||||
revisions: {
|
||||
type: 'json',
|
||||
description: 'File revision history (first 100 revisions only)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveGetFileParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
}
|
||||
|
||||
interface GoogleDriveGetFileResponse extends ToolResponse {
|
||||
output: {
|
||||
file: GoogleDriveFile
|
||||
}
|
||||
}
|
||||
|
||||
export const getFileTool: ToolConfig<GoogleDriveGetFileParams, GoogleDriveGetFileResponse> = {
|
||||
id: 'google_drive_get_file',
|
||||
name: 'Get Google Drive File',
|
||||
description: 'Get metadata for a specific file in Google Drive by its ID',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`)
|
||||
url.searchParams.append('fields', ALL_FILE_FIELDS)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json().catch(() => ({}) as any)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.error?.message ||
|
||||
`Failed to get Google Drive file (${response.status} ${response.statusText})`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'The file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
description: { type: 'string', description: 'File description', optional: true },
|
||||
size: { type: 'string', description: 'File size in bytes', optional: true },
|
||||
starred: { type: 'boolean', description: 'Whether file is starred' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash' },
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
webContentLink: { type: 'string', description: 'Direct download URL', optional: true },
|
||||
iconLink: { type: 'string', description: 'URL to file icon' },
|
||||
thumbnailLink: { type: 'string', description: 'URL to thumbnail', optional: true },
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
owners: { type: 'json', description: 'List of file owners' },
|
||||
permissions: { type: 'json', description: 'File permissions', optional: true },
|
||||
createdTime: { type: 'string', description: 'File creation time' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
lastModifyingUser: { type: 'json', description: 'User who last modified the file' },
|
||||
shared: { type: 'boolean', description: 'Whether file is shared' },
|
||||
ownedByMe: { type: 'boolean', description: 'Whether owned by current user' },
|
||||
capabilities: { type: 'json', description: 'User capabilities on file' },
|
||||
md5Checksum: { type: 'string', description: 'MD5 hash', optional: true },
|
||||
version: { type: 'string', description: 'Version number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { GoogleDriveRevision, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_REVISION_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveGetRevisionParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
revisionId: string
|
||||
}
|
||||
|
||||
interface GoogleDriveGetRevisionResponse extends ToolResponse {
|
||||
output: {
|
||||
revision: GoogleDriveRevision
|
||||
}
|
||||
}
|
||||
|
||||
export const getRevisionTool: ToolConfig<
|
||||
GoogleDriveGetRevisionParams,
|
||||
GoogleDriveGetRevisionResponse
|
||||
> = {
|
||||
id: 'google_drive_get_revision',
|
||||
name: 'Get Google Drive Revision',
|
||||
description: 'Get metadata for a specific revision of a file in Google Drive',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file the revision belongs to',
|
||||
},
|
||||
revisionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the revision to retrieve',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/revisions/${params.revisionId?.trim()}`
|
||||
)
|
||||
url.searchParams.append('fields', ALL_REVISION_FIELDS)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = (await response.json()) as GoogleDriveRevision
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
revision: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
revision: {
|
||||
type: 'json',
|
||||
description: 'The revision metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Revision ID' },
|
||||
mimeType: { type: 'string', description: 'MIME type of the revision' },
|
||||
modifiedTime: { type: 'string', description: 'When this revision was created' },
|
||||
keepForever: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this revision is preserved forever',
|
||||
},
|
||||
published: { type: 'boolean', description: 'Whether this revision is published' },
|
||||
publishedLink: { type: 'string', description: 'Public link to the published revision' },
|
||||
lastModifyingUser: { type: 'json', description: 'User who created this revision' },
|
||||
originalFilename: { type: 'string', description: 'Original filename for binary revisions' },
|
||||
md5Checksum: { type: 'string', description: 'MD5 checksum for binary revisions' },
|
||||
size: { type: 'string', description: 'Size of the revision in bytes' },
|
||||
exportLinks: { type: 'json', description: 'Export format links for the revision' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { copyTool } from '@/tools/google_drive/copy'
|
||||
import { createCommentTool } from '@/tools/google_drive/create_comment'
|
||||
import { createFolderTool } from '@/tools/google_drive/create_folder'
|
||||
import { deleteTool } from '@/tools/google_drive/delete'
|
||||
import { deleteCommentTool } from '@/tools/google_drive/delete_comment'
|
||||
import { downloadTool } from '@/tools/google_drive/download'
|
||||
import { exportTool } from '@/tools/google_drive/export'
|
||||
import { getAboutTool } from '@/tools/google_drive/get_about'
|
||||
import { getContentTool } from '@/tools/google_drive/get_content'
|
||||
import { getFileTool } from '@/tools/google_drive/get_file'
|
||||
import { getRevisionTool } from '@/tools/google_drive/get_revision'
|
||||
import { listTool } from '@/tools/google_drive/list'
|
||||
import { listCommentsTool } from '@/tools/google_drive/list_comments'
|
||||
import { listPermissionsTool } from '@/tools/google_drive/list_permissions'
|
||||
import { listRevisionsTool } from '@/tools/google_drive/list_revisions'
|
||||
import { moveTool } from '@/tools/google_drive/move'
|
||||
import { searchTool } from '@/tools/google_drive/search'
|
||||
import { shareTool } from '@/tools/google_drive/share'
|
||||
import { trashTool } from '@/tools/google_drive/trash'
|
||||
import { unshareTool } from '@/tools/google_drive/unshare'
|
||||
import { untrashTool } from '@/tools/google_drive/untrash'
|
||||
import { updateTool } from '@/tools/google_drive/update'
|
||||
import { uploadTool } from '@/tools/google_drive/upload'
|
||||
|
||||
export const googleDriveCopyTool = copyTool
|
||||
export const googleDriveCreateCommentTool = createCommentTool
|
||||
export const googleDriveCreateFolderTool = createFolderTool
|
||||
export const googleDriveDeleteTool = deleteTool
|
||||
export const googleDriveDeleteCommentTool = deleteCommentTool
|
||||
export const googleDriveDownloadTool = downloadTool
|
||||
export const googleDriveExportTool = exportTool
|
||||
export const googleDriveGetAboutTool = getAboutTool
|
||||
export const googleDriveGetContentTool = getContentTool
|
||||
export const googleDriveGetFileTool = getFileTool
|
||||
export const googleDriveGetRevisionTool = getRevisionTool
|
||||
export const googleDriveListTool = listTool
|
||||
export const googleDriveListCommentsTool = listCommentsTool
|
||||
export const googleDriveListPermissionsTool = listPermissionsTool
|
||||
export const googleDriveListRevisionsTool = listRevisionsTool
|
||||
export const googleDriveMoveTool = moveTool
|
||||
export const googleDriveSearchTool = searchTool
|
||||
export const googleDriveShareTool = shareTool
|
||||
export const googleDriveTrashTool = trashTool
|
||||
export const googleDriveUnshareTool = unshareTool
|
||||
export const googleDriveUntrashTool = untrashTool
|
||||
export const googleDriveUpdateTool = updateTool
|
||||
export const googleDriveUploadTool = uploadTool
|
||||
@@ -0,0 +1,204 @@
|
||||
import type { GoogleDriveListResponse, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse> = {
|
||||
id: 'google_drive_list',
|
||||
name: 'List Google Drive Files',
|
||||
description: 'List files and folders in Google Drive with complete metadata',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Drive API',
|
||||
},
|
||||
folderSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Drive folder ID to list files from (e.g., 1ABCxyz...)',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the folder to list files from (internal use)',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Search term to filter files by name (e.g. "budget" finds files with "budget" in the name). Do NOT use Google Drive query syntax here - just provide a plain search term.',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The maximum number of files to return (default: 100)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The page token to use for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://www.googleapis.com/drive/v3/files')
|
||||
url.searchParams.append('fields', `files(${ALL_FILE_FIELDS}),nextPageToken`)
|
||||
// Ensure shared drives support - corpora=allDrives is critical for searching across shared drives
|
||||
url.searchParams.append('corpora', 'allDrives')
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
url.searchParams.append('includeItemsFromAllDrives', 'true')
|
||||
|
||||
// Helper to escape single quotes for Google Drive query syntax
|
||||
const escapeQueryValue = (value: string): string =>
|
||||
value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
||||
|
||||
// Build the query conditions. `params.query` here is a plain-text name
|
||||
// search term (wrapped in `name contains '...'` below), not Google Drive
|
||||
// query syntax — so there's no caller-supplied `trashed` predicate to
|
||||
// honour. Always exclude trashed files.
|
||||
const conditions: string[] = ['trashed = false']
|
||||
const folderId = (params.folderId || params.folderSelector)?.trim()
|
||||
if (folderId) {
|
||||
const escapedFolderId = escapeQueryValue(folderId)
|
||||
conditions.push(`'${escapedFolderId}' in parents`)
|
||||
}
|
||||
|
||||
// Combine all conditions with AND
|
||||
url.searchParams.append('q', conditions.join(' and '))
|
||||
|
||||
if (params.query) {
|
||||
const existingQ = url.searchParams.get('q')
|
||||
const escapedQuery = escapeQueryValue(params.query)
|
||||
const queryPart = `name contains '${escapedQuery}'`
|
||||
url.searchParams.set('q', `${existingQ} and ${queryPart}`)
|
||||
}
|
||||
if (params.pageSize) {
|
||||
url.searchParams.append('pageSize', Number(params.pageSize).toString())
|
||||
}
|
||||
if (params.pageToken) {
|
||||
url.searchParams.append('pageToken', params.pageToken)
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to list Google Drive files')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
files: data.files ?? [],
|
||||
nextPageToken: data.nextPageToken,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
files: {
|
||||
type: 'array',
|
||||
description: 'Array of file metadata objects from Google Drive',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
description: { type: 'string', description: 'File description' },
|
||||
originalFilename: { type: 'string', description: 'Original uploaded filename' },
|
||||
fullFileExtension: { type: 'string', description: 'Full file extension' },
|
||||
fileExtension: { type: 'string', description: 'File extension' },
|
||||
// Ownership & Sharing
|
||||
owners: { type: 'json', description: 'List of file owners' },
|
||||
permissions: { type: 'json', description: 'File permissions' },
|
||||
permissionIds: { type: 'json', description: 'Permission IDs' },
|
||||
shared: { type: 'boolean', description: 'Whether file is shared' },
|
||||
ownedByMe: { type: 'boolean', description: 'Whether owned by current user' },
|
||||
writersCanShare: { type: 'boolean', description: 'Whether writers can share' },
|
||||
viewersCanCopyContent: { type: 'boolean', description: 'Whether viewers can copy' },
|
||||
copyRequiresWriterPermission: {
|
||||
type: 'boolean',
|
||||
description: 'Whether copy requires writer permission',
|
||||
},
|
||||
sharingUser: { type: 'json', description: 'User who shared the file' },
|
||||
// Labels/Tags
|
||||
starred: { type: 'boolean', description: 'Whether file is starred' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash' },
|
||||
explicitlyTrashed: { type: 'boolean', description: 'Whether explicitly trashed' },
|
||||
properties: { type: 'json', description: 'Custom properties' },
|
||||
appProperties: { type: 'json', description: 'App-specific properties' },
|
||||
// Timestamps
|
||||
createdTime: { type: 'string', description: 'File creation time' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
modifiedByMeTime: { type: 'string', description: 'When modified by current user' },
|
||||
viewedByMeTime: { type: 'string', description: 'When last viewed by current user' },
|
||||
sharedWithMeTime: { type: 'string', description: 'When shared with current user' },
|
||||
// User Info
|
||||
lastModifyingUser: { type: 'json', description: 'User who last modified the file' },
|
||||
viewedByMe: { type: 'boolean', description: 'Whether viewed by current user' },
|
||||
modifiedByMe: { type: 'boolean', description: 'Whether modified by current user' },
|
||||
// Links
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
webContentLink: { type: 'string', description: 'Direct download URL' },
|
||||
iconLink: { type: 'string', description: 'URL to file icon' },
|
||||
thumbnailLink: { type: 'string', description: 'URL to thumbnail' },
|
||||
exportLinks: { type: 'json', description: 'Export format links' },
|
||||
// Size & Storage
|
||||
size: { type: 'string', description: 'File size in bytes' },
|
||||
quotaBytesUsed: { type: 'string', description: 'Storage quota used' },
|
||||
// Checksums
|
||||
md5Checksum: { type: 'string', description: 'MD5 hash' },
|
||||
sha1Checksum: { type: 'string', description: 'SHA-1 hash' },
|
||||
sha256Checksum: { type: 'string', description: 'SHA-256 hash' },
|
||||
// Hierarchy & Location
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
spaces: { type: 'json', description: 'Spaces containing file' },
|
||||
driveId: { type: 'string', description: 'Shared drive ID' },
|
||||
// Capabilities
|
||||
capabilities: { type: 'json', description: 'User capabilities on file' },
|
||||
// Versions
|
||||
version: { type: 'string', description: 'Version number' },
|
||||
headRevisionId: { type: 'string', description: 'Head revision ID' },
|
||||
// Media Metadata
|
||||
hasThumbnail: { type: 'boolean', description: 'Whether has thumbnail' },
|
||||
thumbnailVersion: { type: 'string', description: 'Thumbnail version' },
|
||||
imageMediaMetadata: { type: 'json', description: 'Image-specific metadata' },
|
||||
videoMediaMetadata: { type: 'json', description: 'Video-specific metadata' },
|
||||
// Other
|
||||
isAppAuthorized: { type: 'boolean', description: 'Whether created by requesting app' },
|
||||
contentRestrictions: { type: 'json', description: 'Content restrictions' },
|
||||
linkShareMetadata: { type: 'json', description: 'Link share metadata' },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for fetching the next page of results',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { GoogleDriveComment, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_COMMENT_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveListCommentsParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
includeDeleted?: boolean
|
||||
pageSize?: number
|
||||
pageToken?: string
|
||||
startModifiedTime?: string
|
||||
}
|
||||
|
||||
interface GoogleDriveListCommentsResponse extends ToolResponse {
|
||||
output: {
|
||||
comments: GoogleDriveComment[]
|
||||
nextPageToken?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const listCommentsTool: ToolConfig<
|
||||
GoogleDriveListCommentsParams,
|
||||
GoogleDriveListCommentsResponse
|
||||
> = {
|
||||
id: 'google_drive_list_comments',
|
||||
name: 'List Google Drive Comments',
|
||||
description: 'List comments on a file in Google Drive',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to list comments for',
|
||||
},
|
||||
includeDeleted: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to include deleted comments (their content is stripped)',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of comments to return (1-100, default 20)',
|
||||
},
|
||||
startModifiedTime: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only return comments modified after this RFC 3339 timestamp',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The page token to use for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/comments`
|
||||
)
|
||||
url.searchParams.append('fields', `nextPageToken,comments(${ALL_COMMENT_FIELDS})`)
|
||||
if (params.includeDeleted !== undefined) {
|
||||
url.searchParams.append('includeDeleted', String(params.includeDeleted))
|
||||
}
|
||||
if (params.pageSize) {
|
||||
url.searchParams.append('pageSize', String(params.pageSize))
|
||||
}
|
||||
if (params.startModifiedTime) {
|
||||
url.searchParams.append('startModifiedTime', params.startModifiedTime)
|
||||
}
|
||||
if (params.pageToken) {
|
||||
url.searchParams.append('pageToken', params.pageToken)
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
comments: (data.comments ?? []) as GoogleDriveComment[],
|
||||
nextPageToken: data.nextPageToken,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
comments: {
|
||||
type: 'array',
|
||||
description: 'List of comments on the file',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Comment ID' },
|
||||
content: { type: 'string', description: 'Plain text content of the comment' },
|
||||
htmlContent: { type: 'string', description: 'HTML-formatted content of the comment' },
|
||||
author: { type: 'json', description: 'User who authored the comment' },
|
||||
createdTime: { type: 'string', description: 'When the comment was created' },
|
||||
modifiedTime: { type: 'string', description: 'When the comment was last modified' },
|
||||
resolved: { type: 'boolean', description: 'Whether the comment has been resolved' },
|
||||
deleted: { type: 'boolean', description: 'Whether the comment has been deleted' },
|
||||
anchor: { type: 'string', description: 'Region of the document the comment refers to' },
|
||||
quotedFileContent: {
|
||||
type: 'json',
|
||||
description: 'The file content the comment quotes',
|
||||
},
|
||||
replies: { type: 'json', description: 'Threaded replies to the comment' },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for fetching the next page of comments',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { GoogleDrivePermission, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveListPermissionsParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
interface GoogleDriveListPermissionsResponse extends ToolResponse {
|
||||
output: {
|
||||
permissions: GoogleDrivePermission[]
|
||||
nextPageToken?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const listPermissionsTool: ToolConfig<
|
||||
GoogleDriveListPermissionsParams,
|
||||
GoogleDriveListPermissionsResponse
|
||||
> = {
|
||||
id: 'google_drive_list_permissions',
|
||||
name: 'List Google Drive Permissions',
|
||||
description: 'List all permissions (who has access) for a file in Google Drive',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to list permissions for',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The page token to use for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/permissions`
|
||||
)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
url.searchParams.append(
|
||||
'fields',
|
||||
'nextPageToken,permissions(id,type,role,emailAddress,displayName,photoLink,domain,expirationTime,deleted,allowFileDiscovery,pendingOwner,permissionDetails)'
|
||||
)
|
||||
if (params.pageToken) {
|
||||
url.searchParams.append('pageToken', params.pageToken)
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to list Google Drive permissions')
|
||||
}
|
||||
|
||||
const permissions = (data.permissions ?? []).map((p: Record<string, unknown>) => ({
|
||||
id: p.id ?? null,
|
||||
type: p.type ?? null,
|
||||
role: p.role ?? null,
|
||||
emailAddress: p.emailAddress ?? null,
|
||||
displayName: p.displayName ?? null,
|
||||
photoLink: p.photoLink ?? null,
|
||||
domain: p.domain ?? null,
|
||||
expirationTime: p.expirationTime ?? null,
|
||||
deleted: p.deleted ?? false,
|
||||
allowFileDiscovery: p.allowFileDiscovery ?? null,
|
||||
pendingOwner: p.pendingOwner ?? false,
|
||||
permissionDetails: p.permissionDetails ?? null,
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
permissions,
|
||||
nextPageToken: data.nextPageToken,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
permissions: {
|
||||
type: 'array',
|
||||
description: 'List of permissions on the file',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Permission ID (use to remove permission)' },
|
||||
type: { type: 'string', description: 'Grantee type (user, group, domain, anyone)' },
|
||||
role: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Permission role (owner, organizer, fileOrganizer, writer, commenter, reader)',
|
||||
},
|
||||
emailAddress: { type: 'string', description: 'Email of the grantee' },
|
||||
displayName: { type: 'string', description: 'Display name of the grantee' },
|
||||
photoLink: { type: 'string', description: 'Photo URL of the grantee' },
|
||||
domain: { type: 'string', description: 'Domain of the grantee' },
|
||||
expirationTime: { type: 'string', description: 'When permission expires' },
|
||||
deleted: { type: 'boolean', description: 'Whether grantee account is deleted' },
|
||||
allowFileDiscovery: {
|
||||
type: 'boolean',
|
||||
description: 'Whether file is discoverable by grantee',
|
||||
},
|
||||
pendingOwner: { type: 'boolean', description: 'Whether ownership transfer is pending' },
|
||||
permissionDetails: {
|
||||
type: 'json',
|
||||
description: 'Details about inherited permissions',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for fetching the next page of permissions',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { GoogleDriveRevision, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_REVISION_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveListRevisionsParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
pageSize?: number
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
interface GoogleDriveListRevisionsResponse extends ToolResponse {
|
||||
output: {
|
||||
revisions: GoogleDriveRevision[]
|
||||
nextPageToken?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const listRevisionsTool: ToolConfig<
|
||||
GoogleDriveListRevisionsParams,
|
||||
GoogleDriveListRevisionsResponse
|
||||
> = {
|
||||
id: 'google_drive_list_revisions',
|
||||
name: 'List Google Drive Revisions',
|
||||
description: 'List the revision history of a file in Google Drive',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to list revisions for',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of revisions to return (1-1000, default 200)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The page token to use for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/revisions`
|
||||
)
|
||||
url.searchParams.append('fields', `nextPageToken,revisions(${ALL_REVISION_FIELDS})`)
|
||||
if (params.pageSize) {
|
||||
url.searchParams.append('pageSize', String(params.pageSize))
|
||||
}
|
||||
if (params.pageToken) {
|
||||
url.searchParams.append('pageToken', params.pageToken)
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
revisions: (data.revisions ?? []) as GoogleDriveRevision[],
|
||||
nextPageToken: data.nextPageToken,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
revisions: {
|
||||
type: 'array',
|
||||
description: 'List of revisions for the file (most recent last)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Revision ID' },
|
||||
mimeType: { type: 'string', description: 'MIME type of the revision' },
|
||||
modifiedTime: { type: 'string', description: 'When this revision was created' },
|
||||
keepForever: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this revision is preserved forever',
|
||||
},
|
||||
published: { type: 'boolean', description: 'Whether this revision is published' },
|
||||
publishedLink: { type: 'string', description: 'Public link to the published revision' },
|
||||
lastModifyingUser: {
|
||||
type: 'json',
|
||||
description: 'User who created this revision',
|
||||
},
|
||||
originalFilename: {
|
||||
type: 'string',
|
||||
description: 'Original filename for binary revisions',
|
||||
},
|
||||
md5Checksum: { type: 'string', description: 'MD5 checksum for binary revisions' },
|
||||
size: { type: 'string', description: 'Size of the revision in bytes' },
|
||||
exportLinks: { type: 'json', description: 'Export format links for the revision' },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for fetching the next page of revisions',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveMoveParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
destinationFolderId: string
|
||||
removeFromCurrent?: boolean
|
||||
}
|
||||
|
||||
interface GoogleDriveMoveResponse extends ToolResponse {
|
||||
output: {
|
||||
file: GoogleDriveFile
|
||||
}
|
||||
}
|
||||
|
||||
export const moveTool: ToolConfig<GoogleDriveMoveParams, GoogleDriveMoveResponse> = {
|
||||
id: 'google_drive_move',
|
||||
name: 'Move Google Drive File',
|
||||
description: 'Move a file or folder to a different folder in Google Drive',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file or folder to move',
|
||||
},
|
||||
destinationFolderId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the destination folder',
|
||||
},
|
||||
removeFromCurrent: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Whether to remove the file from its current parent folder (default: true). Set to false to add the file to the destination without removing it from the current location.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: 'https://www.googleapis.com/drive/v3/files',
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
directExecution: async (params) => {
|
||||
const fileId = params.fileId?.trim()
|
||||
const destinationFolderId = params.destinationFolderId?.trim()
|
||||
const removeFromCurrent = params.removeFromCurrent !== false
|
||||
|
||||
if (!fileId) {
|
||||
throw new Error('fileId is required')
|
||||
}
|
||||
if (!destinationFolderId) {
|
||||
throw new Error('destinationFolderId is required')
|
||||
}
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
// Build the PATCH URL with addParents
|
||||
const url = new URL(`https://www.googleapis.com/drive/v3/files/${fileId}`)
|
||||
url.searchParams.append('addParents', destinationFolderId)
|
||||
url.searchParams.append('fields', ALL_FILE_FIELDS)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
|
||||
if (removeFromCurrent) {
|
||||
// Fetch current parents so we can remove them
|
||||
const metadataUrl = new URL(`https://www.googleapis.com/drive/v3/files/${fileId}`)
|
||||
metadataUrl.searchParams.append('fields', 'parents')
|
||||
metadataUrl.searchParams.append('supportsAllDrives', 'true')
|
||||
|
||||
const metadataResponse = await fetch(metadataUrl.toString(), { headers })
|
||||
|
||||
if (!metadataResponse.ok) {
|
||||
const errorData = await metadataResponse.json()
|
||||
throw new Error(errorData.error?.message || 'Failed to retrieve file metadata')
|
||||
}
|
||||
|
||||
const metadata = await metadataResponse.json()
|
||||
if (metadata.parents && metadata.parents.length > 0) {
|
||||
url.searchParams.append('removeParents', metadata.parents.join(','))
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to move Google Drive file')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'The moved file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
createdTime: { type: 'string', description: 'File creation time' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
owners: { type: 'json', description: 'List of file owners' },
|
||||
size: { type: 'string', description: 'File size in bytes' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveSearchParams extends GoogleDriveToolParams {
|
||||
query: string
|
||||
pageSize?: number
|
||||
pageToken?: string
|
||||
}
|
||||
|
||||
interface GoogleDriveSearchResponse extends ToolResponse {
|
||||
output: {
|
||||
files: GoogleDriveFile[]
|
||||
nextPageToken?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const searchTool: ToolConfig<GoogleDriveSearchParams, GoogleDriveSearchResponse> = {
|
||||
id: 'google_drive_search',
|
||||
name: 'Search Google Drive Files',
|
||||
description:
|
||||
'Search for files in Google Drive using advanced query syntax (e.g., fullText contains, mimeType, modifiedTime, etc.)',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Google Drive query string using advanced search syntax (e.g., "fullText contains \'budget\'", "mimeType = \'application/pdf\'", "modifiedTime > \'2024-01-01\'")',
|
||||
},
|
||||
pageSize: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of files to return (default: 100)',
|
||||
},
|
||||
pageToken: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'Token for fetching the next page of results',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL('https://www.googleapis.com/drive/v3/files')
|
||||
url.searchParams.append('fields', `files(${ALL_FILE_FIELDS}),nextPageToken`)
|
||||
url.searchParams.append('corpora', 'allDrives')
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
url.searchParams.append('includeItemsFromAllDrives', 'true')
|
||||
|
||||
// The query is passed directly as Google Drive query syntax
|
||||
const userQuery = params.query?.trim()
|
||||
const userSpecifiesTrashed = userQuery ? /\btrashed\s*=/.test(userQuery) : false
|
||||
const conditions: string[] = []
|
||||
if (!userSpecifiesTrashed) {
|
||||
conditions.push('trashed = false')
|
||||
}
|
||||
if (userQuery) {
|
||||
conditions.push(userQuery)
|
||||
}
|
||||
url.searchParams.append('q', conditions.join(' and '))
|
||||
|
||||
if (params.pageSize) {
|
||||
url.searchParams.append('pageSize', Number(params.pageSize).toString())
|
||||
}
|
||||
if (params.pageToken) {
|
||||
url.searchParams.append('pageToken', params.pageToken)
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to search Google Drive files')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
files: data.files || [],
|
||||
nextPageToken: data.nextPageToken,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
files: {
|
||||
type: 'array',
|
||||
description: 'Array of file metadata objects matching the search query',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
description: { type: 'string', description: 'File description' },
|
||||
originalFilename: { type: 'string', description: 'Original uploaded filename' },
|
||||
fullFileExtension: { type: 'string', description: 'Full file extension' },
|
||||
fileExtension: { type: 'string', description: 'File extension' },
|
||||
owners: { type: 'json', description: 'List of file owners' },
|
||||
permissions: { type: 'json', description: 'File permissions' },
|
||||
shared: { type: 'boolean', description: 'Whether file is shared' },
|
||||
ownedByMe: { type: 'boolean', description: 'Whether owned by current user' },
|
||||
starred: { type: 'boolean', description: 'Whether file is starred' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash' },
|
||||
createdTime: { type: 'string', description: 'File creation time' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
lastModifyingUser: { type: 'json', description: 'User who last modified the file' },
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
webContentLink: { type: 'string', description: 'Direct download URL' },
|
||||
iconLink: { type: 'string', description: 'URL to file icon' },
|
||||
thumbnailLink: { type: 'string', description: 'URL to thumbnail' },
|
||||
size: { type: 'string', description: 'File size in bytes' },
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
driveId: { type: 'string', description: 'Shared drive ID' },
|
||||
capabilities: { type: 'json', description: 'User capabilities on file' },
|
||||
version: { type: 'string', description: 'Version number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for fetching the next page of results',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import type { GoogleDrivePermission, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveShareParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
email?: string
|
||||
domain?: string
|
||||
type: 'user' | 'group' | 'domain' | 'anyone'
|
||||
role: 'owner' | 'organizer' | 'fileOrganizer' | 'writer' | 'commenter' | 'reader'
|
||||
transferOwnership?: boolean
|
||||
moveToNewOwnersRoot?: boolean
|
||||
sendNotification?: boolean
|
||||
emailMessage?: string
|
||||
}
|
||||
|
||||
interface GoogleDriveShareResponse extends ToolResponse {
|
||||
output: {
|
||||
permission: GoogleDrivePermission
|
||||
}
|
||||
}
|
||||
|
||||
export const shareTool: ToolConfig<GoogleDriveShareParams, GoogleDriveShareResponse> = {
|
||||
id: 'google_drive_share',
|
||||
name: 'Share Google Drive File',
|
||||
description: 'Share a file with a user, group, domain, or make it public',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to share',
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Type of grantee: user, group, domain, or anyone',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Permission role: owner (transfer ownership), organizer (shared drive only), fileOrganizer (shared drive only), writer (edit), commenter (view and comment), reader (view only)',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Email address of the user or group (required for type=user or type=group)',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Domain to share with (required for type=domain)',
|
||||
},
|
||||
transferOwnership: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Required when role is owner. Transfers ownership to the specified user.',
|
||||
},
|
||||
moveToNewOwnersRoot: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
"When transferring ownership, move the file to the new owner's My Drive root folder.",
|
||||
},
|
||||
sendNotification: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to send an email notification (default: true)',
|
||||
},
|
||||
emailMessage: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Custom message to include in the notification email',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/permissions`
|
||||
)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
if (params.transferOwnership) {
|
||||
url.searchParams.append('transferOwnership', 'true')
|
||||
}
|
||||
if (params.transferOwnership && params.moveToNewOwnersRoot) {
|
||||
url.searchParams.append('moveToNewOwnersRoot', 'true')
|
||||
}
|
||||
if (params.transferOwnership) {
|
||||
url.searchParams.append('sendNotificationEmail', 'true')
|
||||
} else if (params.sendNotification !== undefined) {
|
||||
url.searchParams.append('sendNotificationEmail', String(params.sendNotification))
|
||||
}
|
||||
if (params.emailMessage) {
|
||||
url.searchParams.append('emailMessage', params.emailMessage)
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
type: params.type,
|
||||
role: params.role,
|
||||
}
|
||||
if (params.email) {
|
||||
body.emailAddress = params.email.trim()
|
||||
}
|
||||
if (params.domain) {
|
||||
body.domain = params.domain.trim()
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to share Google Drive file')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
permission: {
|
||||
id: data.id ?? null,
|
||||
type: data.type ?? null,
|
||||
role: data.role ?? null,
|
||||
emailAddress: data.emailAddress ?? null,
|
||||
displayName: data.displayName ?? null,
|
||||
domain: data.domain ?? null,
|
||||
expirationTime: data.expirationTime ?? null,
|
||||
deleted: data.deleted ?? false,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
permission: {
|
||||
type: 'json',
|
||||
description: 'The created permission details',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Permission ID' },
|
||||
type: { type: 'string', description: 'Grantee type (user, group, domain, anyone)' },
|
||||
role: { type: 'string', description: 'Permission role' },
|
||||
emailAddress: { type: 'string', description: 'Email of the grantee', optional: true },
|
||||
displayName: { type: 'string', description: 'Display name of the grantee', optional: true },
|
||||
domain: { type: 'string', description: 'Domain of the grantee', optional: true },
|
||||
expirationTime: { type: 'string', description: 'Expiration time', optional: true },
|
||||
deleted: { type: 'boolean', description: 'Whether grantee is deleted' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveTrashParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
}
|
||||
|
||||
interface GoogleDriveTrashResponse extends ToolResponse {
|
||||
output: {
|
||||
file: GoogleDriveFile
|
||||
}
|
||||
}
|
||||
|
||||
export const trashTool: ToolConfig<GoogleDriveTrashParams, GoogleDriveTrashResponse> = {
|
||||
id: 'google_drive_trash',
|
||||
name: 'Trash Google Drive File',
|
||||
description: 'Move a file to the trash in Google Drive (can be restored later)',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to move to trash',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`)
|
||||
url.searchParams.append('fields', ALL_FILE_FIELDS)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: () => ({
|
||||
trashed: true,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to trash Google Drive file')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'The trashed file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash (should be true)' },
|
||||
trashedTime: { type: 'string', description: 'When file was trashed' },
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
import type { UserFile } from '@/executor/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
// User information returned in various file metadata fields
|
||||
export interface GoogleDriveUser {
|
||||
displayName?: string
|
||||
emailAddress?: string
|
||||
photoLink?: string
|
||||
permissionId?: string
|
||||
kind?: string
|
||||
me?: boolean
|
||||
}
|
||||
|
||||
// Permission details for a file
|
||||
export interface GoogleDrivePermission {
|
||||
id?: string
|
||||
type?: string // 'user' | 'group' | 'domain' | 'anyone'
|
||||
role?: string // 'owner' | 'organizer' | 'fileOrganizer' | 'writer' | 'commenter' | 'reader'
|
||||
emailAddress?: string
|
||||
displayName?: string
|
||||
photoLink?: string
|
||||
domain?: string
|
||||
expirationTime?: string
|
||||
deleted?: boolean
|
||||
allowFileDiscovery?: boolean
|
||||
pendingOwner?: boolean
|
||||
permissionDetails?: Array<{
|
||||
permissionType?: string
|
||||
role?: string
|
||||
inheritedFrom?: string
|
||||
inherited?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
// Label/tag information
|
||||
interface GoogleDriveLabel {
|
||||
id?: string
|
||||
revisionId?: string
|
||||
kind?: string
|
||||
fields?: Record<
|
||||
string,
|
||||
{
|
||||
kind?: string
|
||||
id?: string
|
||||
valueType?: string
|
||||
dateString?: string[]
|
||||
integer?: string[]
|
||||
selection?: string[]
|
||||
text?: string[]
|
||||
user?: GoogleDriveUser[]
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
// Content hints for indexing
|
||||
interface GoogleDriveContentHints {
|
||||
indexableText?: string
|
||||
thumbnail?: {
|
||||
image?: string
|
||||
mimeType?: string
|
||||
}
|
||||
}
|
||||
|
||||
// Image-specific metadata
|
||||
interface GoogleDriveImageMediaMetadata {
|
||||
width?: number
|
||||
height?: number
|
||||
rotation?: number
|
||||
time?: string
|
||||
cameraMake?: string
|
||||
cameraModel?: string
|
||||
exposureTime?: number
|
||||
aperture?: number
|
||||
flashUsed?: boolean
|
||||
focalLength?: number
|
||||
isoSpeed?: number
|
||||
meteringMode?: string
|
||||
sensor?: string
|
||||
exposureMode?: string
|
||||
colorSpace?: string
|
||||
whiteBalance?: string
|
||||
exposureBias?: number
|
||||
maxApertureValue?: number
|
||||
subjectDistance?: number
|
||||
lens?: string
|
||||
location?: {
|
||||
latitude?: number
|
||||
longitude?: number
|
||||
altitude?: number
|
||||
}
|
||||
}
|
||||
|
||||
// Video-specific metadata
|
||||
interface GoogleDriveVideoMediaMetadata {
|
||||
width?: number
|
||||
height?: number
|
||||
durationMillis?: string
|
||||
}
|
||||
|
||||
// Shortcut details
|
||||
interface GoogleDriveShortcutDetails {
|
||||
targetId?: string
|
||||
targetMimeType?: string
|
||||
targetResourceKey?: string
|
||||
}
|
||||
|
||||
// Content restrictions
|
||||
interface GoogleDriveContentRestriction {
|
||||
readOnly?: boolean
|
||||
reason?: string
|
||||
type?: string
|
||||
restrictingUser?: GoogleDriveUser
|
||||
restrictionTime?: string
|
||||
ownerRestricted?: boolean
|
||||
systemRestricted?: boolean
|
||||
}
|
||||
|
||||
// Link share metadata
|
||||
interface GoogleDriveLinkShareMetadata {
|
||||
securityUpdateEligible?: boolean
|
||||
securityUpdateEnabled?: boolean
|
||||
}
|
||||
|
||||
// Capabilities - what the current user can do with the file
|
||||
interface GoogleDriveCapabilities {
|
||||
canAcceptOwnership?: boolean
|
||||
canAddChildren?: boolean
|
||||
canAddFolderFromAnotherDrive?: boolean
|
||||
canAddMyDriveParent?: boolean
|
||||
canChangeCopyRequiresWriterPermission?: boolean
|
||||
canChangeSecurityUpdateEnabled?: boolean
|
||||
canChangeViewersCanCopyContent?: boolean
|
||||
canComment?: boolean
|
||||
canCopy?: boolean
|
||||
canDelete?: boolean
|
||||
canDeleteChildren?: boolean
|
||||
canDownload?: boolean
|
||||
canEdit?: boolean
|
||||
canListChildren?: boolean
|
||||
canModifyContent?: boolean
|
||||
canModifyContentRestriction?: boolean
|
||||
canModifyEditorContentRestriction?: boolean
|
||||
canModifyLabels?: boolean
|
||||
canModifyOwnerContentRestriction?: boolean
|
||||
canMoveChildrenOutOfDrive?: boolean
|
||||
canMoveChildrenOutOfTeamDrive?: boolean
|
||||
canMoveChildrenWithinDrive?: boolean
|
||||
canMoveChildrenWithinTeamDrive?: boolean
|
||||
canMoveItemIntoTeamDrive?: boolean
|
||||
canMoveItemOutOfDrive?: boolean
|
||||
canMoveItemOutOfTeamDrive?: boolean
|
||||
canMoveItemWithinDrive?: boolean
|
||||
canMoveItemWithinTeamDrive?: boolean
|
||||
canMoveTeamDriveItem?: boolean
|
||||
canReadDrive?: boolean
|
||||
canReadLabels?: boolean
|
||||
canReadRevisions?: boolean
|
||||
canReadTeamDrive?: boolean
|
||||
canRemoveChildren?: boolean
|
||||
canRemoveContentRestriction?: boolean
|
||||
canRemoveMyDriveParent?: boolean
|
||||
canRename?: boolean
|
||||
canShare?: boolean
|
||||
canTrash?: boolean
|
||||
canTrashChildren?: boolean
|
||||
canUntrash?: boolean
|
||||
}
|
||||
|
||||
// Revision information
|
||||
export interface GoogleDriveRevision {
|
||||
id?: string
|
||||
mimeType?: string
|
||||
modifiedTime?: string
|
||||
keepForever?: boolean
|
||||
published?: boolean
|
||||
publishAuto?: boolean
|
||||
publishedLink?: string
|
||||
publishedOutsideDomain?: boolean
|
||||
lastModifyingUser?: GoogleDriveUser
|
||||
originalFilename?: string
|
||||
md5Checksum?: string
|
||||
size?: string
|
||||
exportLinks?: Record<string, string>
|
||||
kind?: string
|
||||
}
|
||||
|
||||
/** A threaded reply attached to a comment. */
|
||||
export interface GoogleDriveCommentReply {
|
||||
id?: string
|
||||
kind?: string
|
||||
createdTime?: string
|
||||
modifiedTime?: string
|
||||
author?: GoogleDriveUser
|
||||
htmlContent?: string
|
||||
content?: string
|
||||
deleted?: boolean
|
||||
action?: string // 'resolve' | 'reopen'
|
||||
}
|
||||
|
||||
/** A comment on a Google Drive file. */
|
||||
export interface GoogleDriveComment {
|
||||
id?: string
|
||||
kind?: string
|
||||
createdTime?: string
|
||||
modifiedTime?: string
|
||||
author?: GoogleDriveUser
|
||||
htmlContent?: string
|
||||
content?: string
|
||||
deleted?: boolean
|
||||
resolved?: boolean
|
||||
anchor?: string
|
||||
quotedFileContent?: {
|
||||
mimeType?: string
|
||||
value?: string
|
||||
}
|
||||
replies?: GoogleDriveCommentReply[]
|
||||
}
|
||||
|
||||
// Complete file metadata - all 50+ fields from Google Drive API v3
|
||||
export interface GoogleDriveFile {
|
||||
// Basic Info
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
kind?: string
|
||||
description?: string
|
||||
originalFilename?: string
|
||||
fullFileExtension?: string
|
||||
fileExtension?: string
|
||||
|
||||
// Ownership & Sharing
|
||||
owners?: GoogleDriveUser[]
|
||||
permissions?: GoogleDrivePermission[]
|
||||
permissionIds?: string[]
|
||||
shared?: boolean
|
||||
ownedByMe?: boolean
|
||||
writersCanShare?: boolean
|
||||
viewersCanCopyContent?: boolean
|
||||
copyRequiresWriterPermission?: boolean
|
||||
sharingUser?: GoogleDriveUser
|
||||
|
||||
// Labels/Tags
|
||||
labels?: GoogleDriveLabel[]
|
||||
labelInfo?: {
|
||||
labels?: GoogleDriveLabel[]
|
||||
}
|
||||
starred?: boolean
|
||||
trashed?: boolean
|
||||
explicitlyTrashed?: boolean
|
||||
properties?: Record<string, string>
|
||||
appProperties?: Record<string, string>
|
||||
folderColorRgb?: string
|
||||
|
||||
// Timestamps
|
||||
createdTime?: string
|
||||
modifiedTime?: string
|
||||
modifiedByMeTime?: string
|
||||
viewedByMeTime?: string
|
||||
sharedWithMeTime?: string
|
||||
trashedTime?: string
|
||||
|
||||
// User Info
|
||||
lastModifyingUser?: GoogleDriveUser
|
||||
trashingUser?: GoogleDriveUser
|
||||
viewedByMe?: boolean
|
||||
modifiedByMe?: boolean
|
||||
|
||||
// Links
|
||||
webViewLink?: string
|
||||
webContentLink?: string
|
||||
iconLink?: string
|
||||
thumbnailLink?: string
|
||||
exportLinks?: Record<string, string>
|
||||
|
||||
// Size & Storage
|
||||
size?: string
|
||||
quotaBytesUsed?: string
|
||||
|
||||
// Checksums
|
||||
md5Checksum?: string
|
||||
sha1Checksum?: string
|
||||
sha256Checksum?: string
|
||||
|
||||
// Hierarchy & Location
|
||||
parents?: string[]
|
||||
spaces?: string[]
|
||||
driveId?: string
|
||||
teamDriveId?: string
|
||||
|
||||
// Capabilities
|
||||
capabilities?: GoogleDriveCapabilities
|
||||
|
||||
// Versions
|
||||
version?: string
|
||||
headRevisionId?: string
|
||||
|
||||
// Media Metadata
|
||||
hasThumbnail?: boolean
|
||||
thumbnailVersion?: string
|
||||
imageMediaMetadata?: GoogleDriveImageMediaMetadata
|
||||
videoMediaMetadata?: GoogleDriveVideoMediaMetadata
|
||||
contentHints?: GoogleDriveContentHints
|
||||
|
||||
// Other
|
||||
isAppAuthorized?: boolean
|
||||
contentRestrictions?: GoogleDriveContentRestriction[]
|
||||
resourceKey?: string
|
||||
shortcutDetails?: GoogleDriveShortcutDetails
|
||||
linkShareMetadata?: GoogleDriveLinkShareMetadata
|
||||
hasAugmentedPermissions?: boolean
|
||||
inheritedPermissionsDisabled?: boolean
|
||||
downloadRestrictions?: {
|
||||
restrictedForReaders?: boolean
|
||||
}
|
||||
|
||||
// Revisions (fetched separately but included in response)
|
||||
revisions?: GoogleDriveRevision[]
|
||||
}
|
||||
|
||||
export interface GoogleDriveListResponse extends ToolResponse {
|
||||
output: {
|
||||
files: GoogleDriveFile[]
|
||||
nextPageToken?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDriveUploadResponse extends ToolResponse {
|
||||
output: {
|
||||
file: GoogleDriveFile
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDriveGetContentResponse extends ToolResponse {
|
||||
output: {
|
||||
content: string
|
||||
metadata: GoogleDriveFile
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDriveDownloadResponse extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}
|
||||
metadata: GoogleDriveFile
|
||||
}
|
||||
}
|
||||
|
||||
export interface GoogleDriveToolParams {
|
||||
accessToken: string
|
||||
folderId?: string
|
||||
folderSelector?: string
|
||||
fileId?: string
|
||||
fileName?: string
|
||||
file?: UserFile
|
||||
content?: string
|
||||
mimeType?: string
|
||||
query?: string
|
||||
pageSize?: number
|
||||
pageToken?: string
|
||||
exportMimeType?: string
|
||||
includeRevisions?: boolean
|
||||
}
|
||||
|
||||
export type GoogleDriveResponse =
|
||||
| GoogleDriveUploadResponse
|
||||
| GoogleDriveGetContentResponse
|
||||
| GoogleDriveDownloadResponse
|
||||
| GoogleDriveListResponse
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveUnshareParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
permissionId: string
|
||||
}
|
||||
|
||||
interface GoogleDriveUnshareResponse extends ToolResponse {
|
||||
output: {
|
||||
removed: boolean
|
||||
fileId: string
|
||||
permissionId: string
|
||||
}
|
||||
}
|
||||
|
||||
export const unshareTool: ToolConfig<GoogleDriveUnshareParams, GoogleDriveUnshareResponse> = {
|
||||
id: 'google_drive_unshare',
|
||||
name: 'Unshare Google Drive File',
|
||||
description: 'Remove a permission from a file (revoke access)',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to modify permissions on',
|
||||
},
|
||||
permissionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the permission to remove (use list_permissions to find this)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(
|
||||
`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}/permissions/${params.permissionId?.trim()}`
|
||||
)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params) => {
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.error?.message || 'Failed to remove permission from Google Drive file')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
removed: true,
|
||||
fileId: params?.fileId ?? '',
|
||||
permissionId: params?.permissionId ?? '',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
removed: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the permission was successfully removed',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the file',
|
||||
},
|
||||
permissionId: {
|
||||
type: 'string',
|
||||
description: 'The ID of the removed permission',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveUntrashParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
}
|
||||
|
||||
interface GoogleDriveUntrashResponse extends ToolResponse {
|
||||
output: {
|
||||
file: GoogleDriveFile
|
||||
}
|
||||
}
|
||||
|
||||
export const untrashTool: ToolConfig<GoogleDriveUntrashParams, GoogleDriveUntrashResponse> = {
|
||||
id: 'google_drive_untrash',
|
||||
name: 'Restore Google Drive File',
|
||||
description: 'Restore a file from the trash in Google Drive',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to restore from trash',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`)
|
||||
url.searchParams.append('fields', ALL_FILE_FIELDS)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
return url.toString()
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: () => ({
|
||||
trashed: false,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error?.message || 'Failed to restore Google Drive file from trash')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'The restored file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash (should be false)' },
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { GoogleDriveFile, GoogleDriveToolParams } from '@/tools/google_drive/types'
|
||||
import { ALL_FILE_FIELDS } from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
interface GoogleDriveUpdateParams extends GoogleDriveToolParams {
|
||||
fileId: string
|
||||
name?: string
|
||||
description?: string
|
||||
addParents?: string
|
||||
removeParents?: string
|
||||
starred?: boolean
|
||||
}
|
||||
|
||||
interface GoogleDriveUpdateResponse extends ToolResponse {
|
||||
output: {
|
||||
file: GoogleDriveFile
|
||||
}
|
||||
}
|
||||
|
||||
export const updateTool: ToolConfig<GoogleDriveUpdateParams, GoogleDriveUpdateResponse> = {
|
||||
id: 'google_drive_update',
|
||||
name: 'Update Google Drive File',
|
||||
description: 'Update file metadata in Google Drive (rename, move, star, add description)',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The ID of the file to update',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New name for the file',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New description for the file',
|
||||
},
|
||||
addParents: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of parent folder IDs to add (moves file to these folders)',
|
||||
},
|
||||
removeParents: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Comma-separated list of parent folder IDs to remove',
|
||||
},
|
||||
starred: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Whether to star or unstar the file',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = new URL(`https://www.googleapis.com/drive/v3/files/${params.fileId?.trim()}`)
|
||||
url.searchParams.append('fields', ALL_FILE_FIELDS)
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
if (params.addParents) {
|
||||
url.searchParams.append('addParents', params.addParents.trim())
|
||||
}
|
||||
if (params.removeParents) {
|
||||
url.searchParams.append('removeParents', params.removeParents.trim())
|
||||
}
|
||||
return url.toString()
|
||||
},
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
if (params.name !== undefined) {
|
||||
body.name = params.name
|
||||
}
|
||||
if (params.description !== undefined) {
|
||||
body.description = params.description
|
||||
}
|
||||
if (params.starred !== undefined) {
|
||||
body.starred = params.starred
|
||||
}
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const data = await response.json().catch(() => ({}) as any)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.error?.message ||
|
||||
`Failed to update Google Drive file (${response.status} ${response.statusText})`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: data,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'json',
|
||||
description: 'The updated file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
description: { type: 'string', description: 'File description', optional: true },
|
||||
starred: { type: 'boolean', description: 'Whether file is starred' },
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { GoogleDriveToolParams, GoogleDriveUploadResponse } from '@/tools/google_drive/types'
|
||||
import {
|
||||
ALL_FILE_FIELDS,
|
||||
GOOGLE_WORKSPACE_MIME_TYPES,
|
||||
handleSheetsFormat,
|
||||
SOURCE_MIME_TYPES,
|
||||
} from '@/tools/google_drive/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('GoogleDriveUploadTool')
|
||||
|
||||
export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResponse> = {
|
||||
id: 'google_drive_upload',
|
||||
name: 'Upload to Google Drive',
|
||||
description: 'Upload a file to Google Drive with complete metadata returned',
|
||||
version: '1.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'google-drive',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'The access token for the Google Drive API',
|
||||
},
|
||||
fileName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'The name of the file to upload',
|
||||
},
|
||||
file: {
|
||||
type: 'file',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Binary file to upload (UserFile object)',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Text content to upload (use this OR file, not both)',
|
||||
},
|
||||
mimeType: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The MIME type of the file to upload (auto-detected from file if not provided)',
|
||||
},
|
||||
folderSelector: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Google Drive folder ID to upload the file to (e.g., 1ABCxyz...)',
|
||||
},
|
||||
folderId: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'hidden',
|
||||
description: 'The ID of the folder to upload the file to (internal use)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Use custom API route if file is provided, otherwise use Google Drive API directly
|
||||
if (params.file) {
|
||||
return '/api/tools/google_drive/upload'
|
||||
}
|
||||
return 'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true'
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params) => {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
// Google Drive API for text-only uploads needs Authorization
|
||||
if (!params.file) {
|
||||
headers.Authorization = `Bearer ${params.accessToken}`
|
||||
}
|
||||
return headers
|
||||
},
|
||||
body: (params) => {
|
||||
// Custom route handles file uploads
|
||||
if (params.file) {
|
||||
return {
|
||||
accessToken: params.accessToken,
|
||||
fileName: params.fileName,
|
||||
file: params.file,
|
||||
mimeType: params.mimeType,
|
||||
folderId: params.folderSelector || params.folderId,
|
||||
}
|
||||
}
|
||||
|
||||
// Original text-only upload logic
|
||||
const metadata: {
|
||||
name: string | undefined
|
||||
mimeType: string
|
||||
parents?: string[]
|
||||
} = {
|
||||
name: params.fileName, // Important: Always include the filename in metadata
|
||||
mimeType: params.mimeType || 'text/plain',
|
||||
}
|
||||
|
||||
// Add parent folder if specified (prefer folderSelector over folderId)
|
||||
const parentFolderId = params.folderSelector || params.folderId
|
||||
if (parentFolderId && parentFolderId.trim() !== '') {
|
||||
metadata.parents = [parentFolderId]
|
||||
}
|
||||
|
||||
return metadata
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: GoogleDriveToolParams) => {
|
||||
try {
|
||||
const data = await response.json()
|
||||
|
||||
// Handle custom API route response (for file uploads)
|
||||
if (params?.file && data.success !== undefined) {
|
||||
if (!data.success) {
|
||||
logger.error('Failed to upload file via custom API route', {
|
||||
error: data.error,
|
||||
})
|
||||
throw new Error(data.error || 'Failed to upload file to Google Drive')
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: data.output.file,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Google Drive API response (for text-only uploads)
|
||||
if (!response.ok) {
|
||||
logger.error('Failed to create file in Google Drive', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data,
|
||||
})
|
||||
throw new Error(data.error?.message || 'Failed to create file in Google Drive')
|
||||
}
|
||||
|
||||
const fileId = data.id
|
||||
const requestedMimeType = params?.mimeType || 'text/plain'
|
||||
const authHeader =
|
||||
response.headers.get('Authorization') || `Bearer ${params?.accessToken || ''}`
|
||||
|
||||
let preparedContent: string | undefined =
|
||||
typeof params?.content === 'string' ? (params?.content as string) : undefined
|
||||
|
||||
if (requestedMimeType === 'application/vnd.google-apps.spreadsheet' && params?.content) {
|
||||
const { csv, rowCount, columnCount } = handleSheetsFormat(params.content as unknown)
|
||||
if (csv !== undefined) {
|
||||
preparedContent = csv
|
||||
logger.info('Prepared CSV content for Google Sheets upload', {
|
||||
fileId,
|
||||
fileName: params?.fileName,
|
||||
rowCount,
|
||||
columnCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const uploadMimeType = GOOGLE_WORKSPACE_MIME_TYPES.includes(requestedMimeType)
|
||||
? SOURCE_MIME_TYPES[requestedMimeType] || 'text/plain'
|
||||
: requestedMimeType
|
||||
|
||||
logger.info('Uploading content to file', {
|
||||
fileId,
|
||||
fileName: params?.fileName,
|
||||
requestedMimeType,
|
||||
uploadMimeType,
|
||||
})
|
||||
|
||||
const uploadResponse = await fetch(
|
||||
`https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=media&supportsAllDrives=true`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
'Content-Type': uploadMimeType,
|
||||
},
|
||||
body: preparedContent !== undefined ? preparedContent : params?.content || '',
|
||||
}
|
||||
)
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
const uploadError = await uploadResponse.json()
|
||||
logger.error('Failed to upload content to file', {
|
||||
status: uploadResponse.status,
|
||||
statusText: uploadResponse.statusText,
|
||||
error: uploadError,
|
||||
})
|
||||
throw new Error(uploadError.error?.message || 'Failed to upload content to file')
|
||||
}
|
||||
|
||||
if (GOOGLE_WORKSPACE_MIME_TYPES.includes(requestedMimeType)) {
|
||||
logger.info('Updating file name to ensure it persists after conversion', {
|
||||
fileId,
|
||||
fileName: params?.fileName,
|
||||
})
|
||||
|
||||
const updateNameResponse = await fetch(
|
||||
`https://www.googleapis.com/drive/v3/files/${fileId}?supportsAllDrives=true`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: params?.fileName,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
if (!updateNameResponse.ok) {
|
||||
logger.warn('Failed to update filename after conversion, but content was uploaded', {
|
||||
status: updateNameResponse.status,
|
||||
statusText: updateNameResponse.statusText,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch complete file metadata with all fields
|
||||
const finalFileResponse = await fetch(
|
||||
`https://www.googleapis.com/drive/v3/files/${fileId}?supportsAllDrives=true&fields=${ALL_FILE_FIELDS}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const finalFile = await finalFileResponse.json()
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: finalFile,
|
||||
},
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Error in upload transformation', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'object',
|
||||
description: 'Complete uploaded file metadata from Google Drive',
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
description: { type: 'string', description: 'File description' },
|
||||
originalFilename: { type: 'string', description: 'Original uploaded filename' },
|
||||
fullFileExtension: { type: 'string', description: 'Full file extension' },
|
||||
fileExtension: { type: 'string', description: 'File extension' },
|
||||
// Ownership & Sharing
|
||||
owners: { type: 'json', description: 'List of file owners' },
|
||||
permissions: { type: 'json', description: 'File permissions' },
|
||||
permissionIds: { type: 'json', description: 'Permission IDs' },
|
||||
shared: { type: 'boolean', description: 'Whether file is shared' },
|
||||
ownedByMe: { type: 'boolean', description: 'Whether owned by current user' },
|
||||
writersCanShare: { type: 'boolean', description: 'Whether writers can share' },
|
||||
viewersCanCopyContent: { type: 'boolean', description: 'Whether viewers can copy' },
|
||||
copyRequiresWriterPermission: {
|
||||
type: 'boolean',
|
||||
description: 'Whether copy requires writer permission',
|
||||
},
|
||||
sharingUser: { type: 'json', description: 'User who shared the file' },
|
||||
// Labels/Tags
|
||||
starred: { type: 'boolean', description: 'Whether file is starred' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash' },
|
||||
explicitlyTrashed: { type: 'boolean', description: 'Whether explicitly trashed' },
|
||||
properties: { type: 'json', description: 'Custom properties' },
|
||||
appProperties: { type: 'json', description: 'App-specific properties' },
|
||||
// Timestamps
|
||||
createdTime: { type: 'string', description: 'File creation time' },
|
||||
modifiedTime: { type: 'string', description: 'Last modification time' },
|
||||
modifiedByMeTime: { type: 'string', description: 'When modified by current user' },
|
||||
viewedByMeTime: { type: 'string', description: 'When last viewed by current user' },
|
||||
sharedWithMeTime: { type: 'string', description: 'When shared with current user' },
|
||||
// User Info
|
||||
lastModifyingUser: { type: 'json', description: 'User who last modified the file' },
|
||||
viewedByMe: { type: 'boolean', description: 'Whether viewed by current user' },
|
||||
modifiedByMe: { type: 'boolean', description: 'Whether modified by current user' },
|
||||
// Links
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
webContentLink: { type: 'string', description: 'Direct download URL' },
|
||||
iconLink: { type: 'string', description: 'URL to file icon' },
|
||||
thumbnailLink: { type: 'string', description: 'URL to thumbnail' },
|
||||
exportLinks: { type: 'json', description: 'Export format links' },
|
||||
// Size & Storage
|
||||
size: { type: 'string', description: 'File size in bytes' },
|
||||
quotaBytesUsed: { type: 'string', description: 'Storage quota used' },
|
||||
// Checksums
|
||||
md5Checksum: { type: 'string', description: 'MD5 hash' },
|
||||
sha1Checksum: { type: 'string', description: 'SHA-1 hash' },
|
||||
sha256Checksum: { type: 'string', description: 'SHA-256 hash' },
|
||||
// Hierarchy & Location
|
||||
parents: { type: 'json', description: 'Parent folder IDs' },
|
||||
spaces: { type: 'json', description: 'Spaces containing file' },
|
||||
driveId: { type: 'string', description: 'Shared drive ID' },
|
||||
// Capabilities
|
||||
capabilities: { type: 'json', description: 'User capabilities on file' },
|
||||
// Versions
|
||||
version: { type: 'string', description: 'Version number' },
|
||||
headRevisionId: { type: 'string', description: 'Head revision ID' },
|
||||
// Media Metadata
|
||||
hasThumbnail: { type: 'boolean', description: 'Whether has thumbnail' },
|
||||
thumbnailVersion: { type: 'string', description: 'Thumbnail version' },
|
||||
imageMediaMetadata: { type: 'json', description: 'Image-specific metadata' },
|
||||
videoMediaMetadata: { type: 'json', description: 'Video-specific metadata' },
|
||||
// Other
|
||||
isAppAuthorized: { type: 'boolean', description: 'Whether created by requesting app' },
|
||||
contentRestrictions: { type: 'json', description: 'Content restrictions' },
|
||||
linkShareMetadata: { type: 'json', description: 'Link share metadata' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// All available file metadata fields from Google Drive API v3
|
||||
export const ALL_FILE_FIELDS = [
|
||||
// Basic Info
|
||||
'id',
|
||||
'name',
|
||||
'mimeType',
|
||||
'kind',
|
||||
'description',
|
||||
'originalFilename',
|
||||
'fullFileExtension',
|
||||
'fileExtension',
|
||||
// Ownership & Sharing
|
||||
'owners',
|
||||
'permissions',
|
||||
'permissionIds',
|
||||
'shared',
|
||||
'ownedByMe',
|
||||
'writersCanShare',
|
||||
'viewersCanCopyContent',
|
||||
'copyRequiresWriterPermission',
|
||||
'sharingUser',
|
||||
// Labels/Tags
|
||||
'starred',
|
||||
'trashed',
|
||||
'explicitlyTrashed',
|
||||
'properties',
|
||||
'appProperties',
|
||||
'folderColorRgb',
|
||||
// Timestamps
|
||||
'createdTime',
|
||||
'modifiedTime',
|
||||
'modifiedByMeTime',
|
||||
'viewedByMeTime',
|
||||
'sharedWithMeTime',
|
||||
'trashedTime',
|
||||
// User Info
|
||||
'lastModifyingUser',
|
||||
'trashingUser',
|
||||
'viewedByMe',
|
||||
'modifiedByMe',
|
||||
// Links
|
||||
'webViewLink',
|
||||
'webContentLink',
|
||||
'iconLink',
|
||||
'thumbnailLink',
|
||||
'exportLinks',
|
||||
// Size & Storage
|
||||
'size',
|
||||
'quotaBytesUsed',
|
||||
// Checksums
|
||||
'md5Checksum',
|
||||
'sha1Checksum',
|
||||
'sha256Checksum',
|
||||
// Hierarchy & Location
|
||||
'parents',
|
||||
'spaces',
|
||||
'driveId',
|
||||
'teamDriveId',
|
||||
// Capabilities
|
||||
'capabilities',
|
||||
// Versions
|
||||
'version',
|
||||
'headRevisionId',
|
||||
// Media Metadata
|
||||
'hasThumbnail',
|
||||
'thumbnailVersion',
|
||||
'imageMediaMetadata',
|
||||
'videoMediaMetadata',
|
||||
'contentHints',
|
||||
// Other
|
||||
'isAppAuthorized',
|
||||
'contentRestrictions',
|
||||
'resourceKey',
|
||||
'shortcutDetails',
|
||||
'linkShareMetadata',
|
||||
'labelInfo',
|
||||
'hasAugmentedPermissions',
|
||||
'inheritedPermissionsDisabled',
|
||||
'downloadRestrictions',
|
||||
].join(',')
|
||||
|
||||
// All revision fields from Google Drive API v3
|
||||
export const ALL_REVISION_FIELDS = [
|
||||
'id',
|
||||
'mimeType',
|
||||
'modifiedTime',
|
||||
'keepForever',
|
||||
'published',
|
||||
'publishAuto',
|
||||
'publishedLink',
|
||||
'publishedOutsideDomain',
|
||||
'lastModifyingUser',
|
||||
'originalFilename',
|
||||
'md5Checksum',
|
||||
'size',
|
||||
'exportLinks',
|
||||
'kind',
|
||||
].join(',')
|
||||
|
||||
/** All reply fields requested from the Google Drive API v3. */
|
||||
const ALL_REPLY_FIELDS = [
|
||||
'id',
|
||||
'kind',
|
||||
'createdTime',
|
||||
'modifiedTime',
|
||||
'author',
|
||||
'htmlContent',
|
||||
'content',
|
||||
'deleted',
|
||||
'action',
|
||||
].join(',')
|
||||
|
||||
/** All comment fields requested from the Google Drive API v3. */
|
||||
export const ALL_COMMENT_FIELDS = [
|
||||
'id',
|
||||
'kind',
|
||||
'createdTime',
|
||||
'modifiedTime',
|
||||
'author',
|
||||
'htmlContent',
|
||||
'content',
|
||||
'deleted',
|
||||
'resolved',
|
||||
'anchor',
|
||||
'quotedFileContent',
|
||||
`replies(${ALL_REPLY_FIELDS})`,
|
||||
].join(',')
|
||||
|
||||
/**
|
||||
* Maximum bytes accepted when exporting a Google Workspace file.
|
||||
* Mirrors Google's own 10 MB export ceiling and keeps memory bounded.
|
||||
*/
|
||||
export const MAX_EXPORT_BYTES = 10 * 1024 * 1024
|
||||
|
||||
export const GOOGLE_WORKSPACE_MIME_TYPES = [
|
||||
'application/vnd.google-apps.document', // Google Docs
|
||||
'application/vnd.google-apps.spreadsheet', // Google Sheets
|
||||
'application/vnd.google-apps.presentation', // Google Slides
|
||||
'application/vnd.google-apps.drawing', // Google Drawings
|
||||
'application/vnd.google-apps.form', // Google Forms
|
||||
'application/vnd.google-apps.script', // Google Apps Scripts
|
||||
]
|
||||
|
||||
export const DEFAULT_EXPORT_FORMATS: Record<string, string> = {
|
||||
'application/vnd.google-apps.document': 'text/plain',
|
||||
'application/vnd.google-apps.spreadsheet': 'text/csv',
|
||||
'application/vnd.google-apps.presentation': 'text/plain',
|
||||
'application/vnd.google-apps.drawing': 'image/png',
|
||||
'application/vnd.google-apps.form': 'application/zip',
|
||||
'application/vnd.google-apps.script': 'application/vnd.google-apps.script+json',
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid export formats per Google Workspace file type.
|
||||
* See: https://developers.google.com/drive/api/guides/ref-export-formats
|
||||
*/
|
||||
export const VALID_EXPORT_FORMATS: Record<string, string[]> = {
|
||||
'application/vnd.google-apps.document': [
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.oasis.opendocument.text',
|
||||
'application/rtf',
|
||||
'application/epub+zip',
|
||||
'text/markdown',
|
||||
],
|
||||
'application/vnd.google-apps.spreadsheet': [
|
||||
'text/csv',
|
||||
'text/tab-separated-values',
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'application/zip',
|
||||
],
|
||||
'application/vnd.google-apps.presentation': [
|
||||
'text/plain',
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.oasis.opendocument.presentation',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
],
|
||||
'application/vnd.google-apps.drawing': [
|
||||
'application/pdf',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/svg+xml',
|
||||
],
|
||||
'application/vnd.google-apps.form': ['application/zip'],
|
||||
'application/vnd.google-apps.script': ['application/vnd.google-apps.script+json'],
|
||||
}
|
||||
|
||||
export const SOURCE_MIME_TYPES: Record<string, string> = {
|
||||
'application/vnd.google-apps.document': 'text/plain',
|
||||
'application/vnd.google-apps.spreadsheet': 'text/csv',
|
||||
'application/vnd.google-apps.presentation': 'application/vnd.ms-powerpoint',
|
||||
}
|
||||
|
||||
export function handleSheetsFormat(input: unknown): {
|
||||
csv?: string
|
||||
rowCount: number
|
||||
columnCount: number
|
||||
} {
|
||||
let workingValue: unknown = input
|
||||
|
||||
if (typeof workingValue === 'string') {
|
||||
try {
|
||||
workingValue = JSON.parse(workingValue)
|
||||
} catch (_error) {
|
||||
const csvString = workingValue as string
|
||||
return { csv: csvString, rowCount: 0, columnCount: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(workingValue)) {
|
||||
return { rowCount: 0, columnCount: 0 }
|
||||
}
|
||||
|
||||
let table: unknown[] = workingValue
|
||||
|
||||
if (
|
||||
table.length > 0 &&
|
||||
typeof (table as any)[0] === 'object' &&
|
||||
(table as any)[0] !== null &&
|
||||
!Array.isArray((table as any)[0])
|
||||
) {
|
||||
const allKeys = new Set<string>()
|
||||
;(table as any[]).forEach((obj) => {
|
||||
if (obj && typeof obj === 'object') {
|
||||
Object.keys(obj).forEach((key) => allKeys.add(key))
|
||||
}
|
||||
})
|
||||
const headers = Array.from(allKeys)
|
||||
const rows = (table as any[]).map((obj) => {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return Array(headers.length).fill('')
|
||||
}
|
||||
return headers.map((key) => {
|
||||
const value = (obj as Record<string, unknown>)[key]
|
||||
if (value !== null && typeof value === 'object') {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
return value === undefined ? '' : (value as any)
|
||||
})
|
||||
})
|
||||
table = [headers, ...rows]
|
||||
}
|
||||
|
||||
const escapeCell = (cell: unknown): string => {
|
||||
if (cell === null || cell === undefined) return ''
|
||||
const stringValue = String(cell)
|
||||
const mustQuote = /[",\n\r]/.test(stringValue)
|
||||
const doubledQuotes = stringValue.replace(/"/g, '""')
|
||||
return mustQuote ? `"${doubledQuotes}"` : doubledQuotes
|
||||
}
|
||||
|
||||
const rowsAsStrings = (table as unknown[]).map((row) => {
|
||||
if (!Array.isArray(row)) {
|
||||
return escapeCell(row)
|
||||
}
|
||||
return row.map((cell) => escapeCell(cell)).join(',')
|
||||
})
|
||||
|
||||
const csv = rowsAsStrings.join('\r\n')
|
||||
const rowCount = Array.isArray(table) ? (table as any[]).length : 0
|
||||
const columnCount =
|
||||
Array.isArray(table) && Array.isArray((table as any[])[0]) ? (table as any[])[0].length : 0
|
||||
|
||||
return { csv, rowCount, columnCount }
|
||||
}
|
||||
Reference in New Issue
Block a user