diff --git a/apps/sim/app/api/tools/file/manage/route.ts b/apps/sim/app/api/tools/file/manage/route.ts index 3880e587bc..e52df34cfc 100644 --- a/apps/sim/app/api/tools/file/manage/route.ts +++ b/apps/sim/app/api/tools/file/manage/route.ts @@ -8,6 +8,7 @@ import { ensureAbsoluteUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { fetchWorkspaceFileBuffer, + getWorkspaceFile, getWorkspaceFileByName, updateWorkspaceFileContent, uploadWorkspaceFile, @@ -40,6 +41,51 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { switch (body.operation) { + case 'get': { + const { fileId, fileInput } = body + const selectedFileId = + fileId || + (fileInput && typeof fileInput === 'object' && !Array.isArray(fileInput) + ? typeof fileInput.id === 'string' + ? fileInput.id + : typeof fileInput.fileId === 'string' + ? fileInput.fileId + : '' + : '') + + if (!selectedFileId) { + return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 }) + } + + const file = await getWorkspaceFile(workspaceId, selectedFileId) + if (!file) { + return NextResponse.json( + { success: false, error: `File not found: "${selectedFileId}"` }, + { status: 404 } + ) + } + + logger.info('File retrieved', { + fileId: file.id, + name: file.name, + }) + + return NextResponse.json({ + success: true, + data: { + file: { + id: file.id, + name: file.name, + url: ensureAbsoluteUrl(file.path), + size: file.size, + type: file.type, + key: file.key, + context: 'workspace', + }, + }, + }) + } + case 'write': { const { fileName, content, contentType } = body const mimeType = contentType || getMimeTypeFromExtension(getFileExtension(fileName)) diff --git a/apps/sim/blocks/blocks/file.ts b/apps/sim/blocks/blocks/file.ts index 4be2f20bbd..efefb32982 100644 --- a/apps/sim/blocks/blocks/file.ts +++ b/apps/sim/blocks/blocks/file.ts @@ -266,6 +266,7 @@ export const FileV3Block: BlockConfig = { type: 'dropdown' as SubBlockType, options: [ { label: 'Read', id: 'file_parser_v3' }, + { label: 'Get', id: 'file_get' }, { label: 'Write', id: 'file_write' }, { label: 'Append', id: 'file_append' }, ], @@ -294,6 +295,28 @@ export const FileV3Block: BlockConfig = { required: { field: 'operation', value: 'file_parser_v3' }, condition: { field: 'operation', value: 'file_parser_v3' }, }, + { + id: 'getFile', + title: 'File', + type: 'file-upload' as SubBlockType, + canonicalParamId: 'getFileInput', + acceptedTypes: '*', + placeholder: 'Select a workspace file', + multiple: false, + mode: 'basic', + condition: { field: 'operation', value: 'file_get' }, + required: { field: 'operation', value: 'file_get' }, + }, + { + id: 'getFileId', + title: 'File ID', + type: 'short-input' as SubBlockType, + canonicalParamId: 'getFileInput', + placeholder: 'Workspace file ID', + mode: 'advanced', + condition: { field: 'operation', value: 'file_get' }, + required: { field: 'operation', value: 'file_get' }, + }, { id: 'fileName', title: 'File Name', @@ -349,7 +372,7 @@ export const FileV3Block: BlockConfig = { }, ], tools: { - access: ['file_parser_v3', 'file_write', 'file_append'], + access: ['file_parser_v3', 'file_get', 'file_write', 'file_append'], config: { tool: (params) => params.operation || 'file_parser_v3', params: (params) => { @@ -390,6 +413,25 @@ export const FileV3Block: BlockConfig = { } } + if (operation === 'file_get') { + const getInput = params.getFileInput + if (!getInput) { + throw new Error('File is required for get') + } + + if (typeof getInput === 'string') { + return { + fileId: getInput.trim(), + workspaceId: params._context?.workspaceId, + } + } + + return { + fileInput: normalizeFileInput(getInput, { single: true }), + workspaceId: params._context?.workspaceId, + } + } + const fileInput = params.fileInput if (!fileInput) { logger.error('No file input provided') @@ -428,9 +470,13 @@ export const FileV3Block: BlockConfig = { }, }, inputs: { - operation: { type: 'string', description: 'Operation to perform (read, write, or append)' }, + operation: { + type: 'string', + description: 'Operation to perform (read, get, write, or append)', + }, fileInput: { type: 'json', description: 'File input for read' }, fileType: { type: 'string', description: 'File type for read' }, + getFileInput: { type: 'json', description: 'Selected file or workspace file ID for get' }, fileName: { type: 'string', description: 'Name for a new file (write)' }, content: { type: 'string', description: 'File content to write' }, contentType: { type: 'string', description: 'MIME content type for write' }, @@ -446,6 +492,10 @@ export const FileV3Block: BlockConfig = { type: 'string', description: 'All file contents merged into a single text string (read)', }, + file: { + type: 'file', + description: 'Workspace file object (get)', + }, id: { type: 'string', description: 'File ID (write)', diff --git a/apps/sim/lib/api/contracts/tools/file.ts b/apps/sim/lib/api/contracts/tools/file.ts index 8987301ad4..7120d38d15 100644 --- a/apps/sim/lib/api/contracts/tools/file.ts +++ b/apps/sim/lib/api/contracts/tools/file.ts @@ -22,9 +22,21 @@ export const fileManageAppendBodySchema = z.object({ content: z.string({ error: 'content is required for append operation' }), }) -export const fileManageBodySchema = z.discriminatedUnion('operation', [ +export const fileManageGetBodySchema = z + .object({ + operation: z.literal('get'), + workspaceId: z.string().min(1).optional(), + fileId: z.string().min(1).optional(), + fileInput: z.any().optional(), + }) + .refine((data) => data.fileId !== undefined || data.fileInput !== undefined, { + message: 'Either fileId or fileInput is required for get operation', + }) + +export const fileManageBodySchema = z.union([ fileManageWriteBodySchema, fileManageAppendBodySchema, + fileManageGetBodySchema, ]) export const fileManageContract = defineRouteContract({ diff --git a/apps/sim/tools/file/get.ts b/apps/sim/tools/file/get.ts new file mode 100644 index 0000000000..6f05dbca7d --- /dev/null +++ b/apps/sim/tools/file/get.ts @@ -0,0 +1,54 @@ +import type { ToolConfig, ToolResponse, WorkflowToolExecutionContext } from '@/tools/types' + +interface FileGetParams { + fileId?: string + fileInput?: unknown + workspaceId?: string + _context?: WorkflowToolExecutionContext +} + +export const fileGetTool: ToolConfig = { + id: 'file_get', + name: 'File Get', + description: 'Get a workspace file object from a selected file or canonical workspace file ID.', + version: '1.0.0', + + params: { + fileId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Canonical workspace file ID.', + }, + fileInput: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'Selected workspace file object.', + }, + }, + + request: { + url: '/api/tools/file/manage', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + body: (params) => ({ + operation: 'get', + fileId: params.fileId, + fileInput: params.fileInput, + workspaceId: params.workspaceId || params._context?.workspaceId, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + if (!response.ok || !data.success) { + return { success: false, output: {}, error: data.error || 'Failed to get file' } + } + return { success: true, output: data.data } + }, + + outputs: { + file: { type: 'file', description: 'Workspace file object' }, + }, +} diff --git a/apps/sim/tools/file/index.ts b/apps/sim/tools/file/index.ts index 2a60ea594f..4e0b6daed0 100644 --- a/apps/sim/tools/file/index.ts +++ b/apps/sim/tools/file/index.ts @@ -1,6 +1,7 @@ import { fileParserTool, fileParserV2Tool, fileParserV3Tool } from '@/tools/file/parser' export { fileAppendTool } from '@/tools/file/append' +export { fileGetTool } from '@/tools/file/get' export { fileWriteTool } from '@/tools/file/write' export const fileParseTool = fileParserTool diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 4fbaff816b..014ca723df 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -643,6 +643,7 @@ import { } from '@/tools/fathom' import { fileAppendTool, + fileGetTool, fileParserV2Tool, fileParserV3Tool, fileParseTool, @@ -3213,6 +3214,7 @@ export const tools: Record = { file_parser_v2: fileParserV2Tool, file_parser_v3: fileParserV3Tool, file_append: fileAppendTool, + file_get: fileGetTool, file_write: fileWriteTool, firecrawl_scrape: firecrawlScrapeTool, firecrawl_search: firecrawlSearchTool,