more integrations

This commit is contained in:
Vikhyath Mondreti
2026-02-02 01:08:38 -08:00
parent 1da3407f41
commit 39ca1f61c7
67 changed files with 583 additions and 79 deletions

View File

@@ -7,6 +7,7 @@ import binaryExtensionsList from 'binary-extensions'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { secureFetchWithPinnedIP, validateUrlWithDNS } from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { isSupportedFileType, parseFile } from '@/lib/file-parsers'
import { isUsingCloudStorage, type StorageContext, StorageService } from '@/lib/uploads'
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
@@ -367,7 +368,7 @@ async function handleExternalUrl(
throw new Error(`File too large: ${buffer.length} bytes (max: ${MAX_DOWNLOAD_SIZE_BYTES})`)
}
logger.info(`Downloaded file from URL: ${url}, size: ${buffer.length} bytes`)
logger.info(`Downloaded file from URL: ${sanitizeUrlForLog(url)}, size: ${buffer.length} bytes`)
let userFile: UserFile | undefined
const mimeType = response.headers.get('content-type') || getMimeTypeFromExtension(extension)
@@ -420,7 +421,7 @@ async function handleExternalUrl(
return parseResult
} catch (error) {
logger.error(`Error handling external URL ${url}:`, error)
logger.error(`Error handling external URL ${sanitizeUrlForLog(url)}:`, error)
return {
success: false,
error: `Error fetching URL: ${(error as Error).message}`,

View File

@@ -102,6 +102,12 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Processing ${validatedData.files.length} file(s)`)
const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger)
const filesOutput: Array<{
name: string
mimeType: string
data: string
size: number
}> = []
if (userFiles.length === 0) {
logger.warn(`[${requestId}] No valid files to upload, falling back to text-only`)
@@ -138,6 +144,12 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`)
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
filesOutput.push({
name: userFile.name,
mimeType: userFile.type || 'application/octet-stream',
data: buffer.toString('base64'),
size: buffer.length,
})
const blob = new Blob([new Uint8Array(buffer)], { type: userFile.type })
formData.append(`files[${i}]`, blob, userFile.name)
@@ -174,6 +186,7 @@ export async function POST(request: NextRequest) {
message: data.content,
data: data,
fileCount: userFiles.length,
files: filesOutput,
},
})
} catch (error) {

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateImageUrl } from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { generateRequestId } from '@/lib/core/utils/request'
const logger = createLogger('ImageProxyAPI')
@@ -29,13 +30,13 @@ export async function GET(request: NextRequest) {
const urlValidation = validateImageUrl(imageUrl)
if (!urlValidation.isValid) {
logger.warn(`[${requestId}] Blocked image proxy request`, {
url: imageUrl.substring(0, 100),
url: sanitizeUrlForLog(imageUrl),
error: urlValidation.error,
})
return new NextResponse(urlValidation.error || 'Invalid image URL', { status: 403 })
}
logger.info(`[${requestId}] Proxying image request for: ${imageUrl}`)
logger.info(`[${requestId}] Proxying image request for: ${sanitizeUrlForLog(imageUrl)}`)
try {
const imageResponse = await fetch(imageUrl, {

View File

@@ -0,0 +1,121 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { getJiraCloudId } from '@/tools/jira/utils'
const logger = createLogger('JiraAddAttachmentAPI')
export const dynamic = 'force-dynamic'
const JiraAddAttachmentSchema = z.object({
accessToken: z.string().min(1, 'Access token is required'),
domain: z.string().min(1, 'Domain is required'),
issueKey: z.string().min(1, 'Issue key is required'),
files: RawFileInputArraySchema,
cloudId: z.string().optional().nullable(),
})
export async function POST(request: NextRequest) {
const requestId = `jira-attach-${Date.now()}`
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
return NextResponse.json(
{ success: false, error: authResult.error || 'Unauthorized' },
{ status: 401 }
)
}
const body = await request.json()
const validatedData = JiraAddAttachmentSchema.parse(body)
const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger)
if (userFiles.length === 0) {
return NextResponse.json(
{ success: false, error: 'No valid files provided for upload' },
{ status: 400 }
)
}
const cloudId =
validatedData.cloudId ||
(await getJiraCloudId(validatedData.domain, validatedData.accessToken))
const formData = new FormData()
const filesOutput: Array<{ name: string; mimeType: string; data: string; size: number }> = []
for (const file of userFiles) {
const buffer = await downloadFileFromStorage(file, requestId, logger)
filesOutput.push({
name: file.name,
mimeType: file.type || 'application/octet-stream',
data: buffer.toString('base64'),
size: buffer.length,
})
const blob = new Blob([new Uint8Array(buffer)], {
type: file.type || 'application/octet-stream',
})
formData.append('file', blob, file.name)
}
const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${validatedData.issueKey}/attachments`
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
'X-Atlassian-Token': 'no-check',
},
body: formData,
})
if (!response.ok) {
const errorText = await response.text()
logger.error(`[${requestId}] Jira attachment upload failed`, {
status: response.status,
statusText: response.statusText,
error: errorText,
})
return NextResponse.json(
{
success: false,
error: `Failed to upload attachments: ${response.statusText}`,
},
{ status: response.status }
)
}
const attachments = await response.json()
const attachmentIds = Array.isArray(attachments)
? attachments.map((attachment) => attachment.id).filter(Boolean)
: []
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueKey: validatedData.issueKey,
attachmentIds,
files: filesOutput,
},
})
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
logger.error(`[${requestId}] Jira attachment upload error`, error)
return NextResponse.json(
{ success: false, error: error instanceof Error ? error.message : 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
@@ -62,7 +63,7 @@ export async function POST(request: NextRequest) {
const url = `${baseUrl}/servicedesk/${serviceDeskId}/queue${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching queues from:', url)
logger.info('Fetching queues from:', sanitizeUrlForLog(url))
const response = await fetch(url, {
method: 'GET',

View File

@@ -6,6 +6,7 @@ import {
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
@@ -66,7 +67,7 @@ export async function POST(request: NextRequest) {
}
const url = `${baseUrl}/request`
logger.info('Creating request at:', url)
logger.info('Creating request at:', sanitizeUrlForLog(url))
const requestBody: Record<string, unknown> = {
serviceDeskId,
@@ -128,7 +129,7 @@ export async function POST(request: NextRequest) {
const url = `${baseUrl}/request/${issueIdOrKey}`
logger.info('Fetching request from:', url)
logger.info('Fetching request from:', sanitizeUrlForLog(url))
const response = await fetch(url, {
method: 'GET',

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
@@ -68,7 +69,7 @@ export async function POST(request: NextRequest) {
const url = `${baseUrl}/request${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching requests from:', url)
logger.info('Fetching requests from:', sanitizeUrlForLog(url))
const response = await fetch(url, {
method: 'GET',

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
@@ -53,7 +54,7 @@ export async function POST(request: NextRequest) {
const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching request types from:', url)
logger.info('Fetching request types from:', sanitizeUrlForLog(url))
const response = await fetch(url, {
method: 'GET',

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
@@ -43,7 +44,7 @@ export async function POST(request: NextRequest) {
const url = `${baseUrl}/servicedesk${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching service desks from:', url)
logger.info('Fetching service desks from:', sanitizeUrlForLog(url))
const response = await fetch(url, {
method: 'GET',

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
@@ -53,7 +54,7 @@ export async function POST(request: NextRequest) {
const url = `${baseUrl}/request/${issueIdOrKey}/sla${params.toString() ? `?${params.toString()}` : ''}`
logger.info('Fetching SLA info from:', url)
logger.info('Fetching SLA info from:', sanitizeUrlForLog(url))
const response = await fetch(url, {
method: 'GET',

View File

@@ -6,6 +6,7 @@ import {
validateJiraCloudId,
validateJiraIssueKey,
} from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
@@ -69,7 +70,7 @@ export async function POST(request: NextRequest) {
const url = `${baseUrl}/request/${issueIdOrKey}/transition`
logger.info('Transitioning request at:', url)
logger.info('Transitioning request at:', sanitizeUrlForLog(url))
const body: Record<string, unknown> = {
id: transitionId,

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
@@ -49,7 +50,7 @@ export async function POST(request: NextRequest) {
const url = `${baseUrl}/request/${issueIdOrKey}/transition`
logger.info('Fetching transitions from:', url)
logger.info('Fetching transitions from:', sanitizeUrlForLog(url))
const response = await fetch(url, {
method: 'GET',

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
@@ -55,6 +56,12 @@ export async function POST(request: NextRequest) {
})
const attachments: any[] = []
const filesOutput: Array<{
name: string
mimeType: string
data: string
size: number
}> = []
if (validatedData.files && validatedData.files.length > 0) {
const rawFiles = validatedData.files
logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to OneDrive`)
@@ -66,13 +73,19 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`)
const buffer = await downloadFileFromStorage(file, requestId, logger)
filesOutput.push({
name: file.name,
mimeType: file.type || 'application/octet-stream',
data: buffer.toString('base64'),
size: buffer.length,
})
const uploadUrl =
'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' +
encodeURIComponent(file.name) +
':/content'
logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`)
logger.info(`[${requestId}] Uploading to Teams: ${sanitizeUrlForLog(uploadUrl)}`)
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
@@ -238,6 +251,7 @@ export async function POST(request: NextRequest) {
url: responseData.webUrl || '',
attachmentCount: attachments.length,
},
files: filesOutput,
},
})
} catch (error) {

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
@@ -53,6 +54,12 @@ export async function POST(request: NextRequest) {
})
const attachments: any[] = []
const filesOutput: Array<{
name: string
mimeType: string
data: string
size: number
}> = []
if (validatedData.files && validatedData.files.length > 0) {
const rawFiles = validatedData.files
logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to Teams`)
@@ -64,13 +71,19 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`)
const buffer = await downloadFileFromStorage(file, requestId, logger)
filesOutput.push({
name: file.name,
mimeType: file.type || 'application/octet-stream',
data: buffer.toString('base64'),
size: buffer.length,
})
const uploadUrl =
'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' +
encodeURIComponent(file.name) +
':/content'
logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`)
logger.info(`[${requestId}] Uploading to Teams: ${sanitizeUrlForLog(uploadUrl)}`)
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',
@@ -234,6 +247,7 @@ export async function POST(request: NextRequest) {
url: responseData.webUrl || '',
attachmentCount: attachments.length,
},
files: filesOutput,
},
})
} catch (error) {

View File

@@ -5,6 +5,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { StorageService } from '@/lib/uploads'
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
import {
extractStorageKey,
inferContextFromKey,
@@ -19,7 +20,7 @@ const logger = createLogger('MistralParseAPI')
const MistralParseSchema = z.object({
apiKey: z.string().min(1, 'API key is required'),
filePath: z.string().min(1, 'File path is required').optional(),
fileData: z.unknown().optional(),
fileData: FileInputSchema.optional(),
resultType: z.string().optional(),
pages: z.array(z.number()).optional(),
includeImageBase64: z.boolean().optional(),

View File

@@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
import { createSftpConnection, getSftp, isPathSafe, sanitizePath } from '@/app/api/tools/sftp/utils'
export const dynamic = 'force-dynamic'
@@ -111,6 +112,8 @@ export async function POST(request: NextRequest) {
const buffer = Buffer.concat(chunks)
const fileName = path.basename(remotePath)
const extension = getFileExtension(fileName)
const mimeType = getMimeTypeFromExtension(extension)
let content: string
if (params.encoding === 'base64') {
@@ -124,6 +127,12 @@ export async function POST(request: NextRequest) {
return NextResponse.json({
success: true,
fileName,
file: {
name: fileName,
mimeType,
data: buffer.toString('base64'),
size: buffer.length,
},
content,
size: buffer.length,
encoding: params.encoding,

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { generateRequestId } from '@/lib/core/utils/request'
import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
@@ -144,7 +145,7 @@ export async function POST(request: NextRequest) {
const uploadUrl = `https://graph.microsoft.com/v1.0/sites/${validatedData.siteId}/drives/${effectiveDriveId}/root:${encodedPath}:/content`
logger.info(`[${requestId}] Uploading to: ${uploadUrl}`)
logger.info(`[${requestId}] Uploading to: ${sanitizeUrlForLog(uploadUrl)}`)
const uploadResponse = await fetch(uploadUrl, {
method: 'PUT',

View File

@@ -1,6 +1,7 @@
import type { Logger } from '@sim/logger'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import type { ToolFileData } from '@/tools/types'
/**
* Sends a message to a Slack channel using chat.postMessage
@@ -70,14 +71,21 @@ export async function uploadFilesToSlack(
accessToken: string,
requestId: string,
logger: Logger
): Promise<string[]> {
): Promise<{ fileIds: string[]; files: ToolFileData[] }> {
const userFiles = processFilesToUserFiles(files, requestId, logger)
const uploadedFileIds: string[] = []
const uploadedFiles: ToolFileData[] = []
for (const userFile of userFiles) {
logger.info(`[${requestId}] Uploading file: ${userFile.name}`)
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
uploadedFiles.push({
name: userFile.name,
mimeType: userFile.type || 'application/octet-stream',
data: buffer.toString('base64'),
size: buffer.length,
})
const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', {
method: 'POST',
@@ -114,7 +122,7 @@ export async function uploadFilesToSlack(
uploadedFileIds.push(urlData.file_id)
}
return uploadedFileIds
return { fileIds: uploadedFileIds, files: uploadedFiles }
}
/**
@@ -217,7 +225,13 @@ export async function sendSlackMessage(
logger: Logger
): Promise<{
success: boolean
output?: { message: any; ts: string; channel: string; fileCount?: number }
output?: {
message: any
ts: string
channel: string
fileCount?: number
files?: ToolFileData[]
}
error?: string
}> {
const { accessToken, text, threadTs, files } = params
@@ -249,10 +263,15 @@ export async function sendSlackMessage(
// Process files
logger.info(`[${requestId}] Processing ${files.length} file(s)`)
const uploadedFileIds = await uploadFilesToSlack(files, accessToken, requestId, logger)
const { fileIds, files: uploadedFiles } = await uploadFilesToSlack(
files,
accessToken,
requestId,
logger
)
// No valid files uploaded - send text-only
if (uploadedFileIds.length === 0) {
if (fileIds.length === 0) {
logger.warn(`[${requestId}] No valid files to upload, sending text-only message`)
const data = await postSlackMessage(accessToken, channel, text, threadTs)
@@ -265,7 +284,7 @@ export async function sendSlackMessage(
}
// Complete file upload
const completeData = await completeSlackFileUpload(uploadedFileIds, channel, text, accessToken)
const completeData = await completeSlackFileUpload(fileIds, channel, text, accessToken)
if (!completeData.ok) {
logger.error(`[${requestId}] Failed to complete upload:`, completeData.error)
@@ -282,7 +301,8 @@ export async function sendSlackMessage(
message: fileMessage,
ts: fileMessage.ts,
channel,
fileCount: uploadedFileIds.length,
fileCount: fileIds.length,
files: uploadedFiles,
},
}
}

View File

@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import type { Client, SFTPWrapper } from 'ssh2'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils'
const logger = createLogger('SSHDownloadFileAPI')
@@ -96,6 +97,8 @@ export async function POST(request: NextRequest) {
})
const fileName = path.basename(remotePath)
const extension = getFileExtension(fileName)
const mimeType = getMimeTypeFromExtension(extension)
// Encode content as base64 for binary safety
const base64Content = content.toString('base64')
@@ -104,6 +107,12 @@ export async function POST(request: NextRequest) {
return NextResponse.json({
downloaded: true,
file: {
name: fileName,
mimeType,
data: base64Content,
size: stats.size,
},
content: base64Content,
fileName: fileName,
remotePath: remotePath,

View File

@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { env } from '@/lib/core/config/env'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils'
const logger = createLogger('StagehandExtractAPI')
@@ -120,7 +121,7 @@ export async function POST(request: NextRequest) {
const page = stagehand.context.pages()[0]
logger.info(`Navigating to ${url}`)
logger.info(`Navigating to ${sanitizeUrlForLog(url)}`)
await page.goto(url, { waitUntil: 'networkidle' })
logger.info('Navigation complete')

View File

@@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { extractAudioFromVideo, isVideoFile } from '@/lib/audio/extractor'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import type { UserFile } from '@/executor/types'
import type { TranscriptSegment } from '@/tools/stt/types'
@@ -88,7 +89,7 @@ export async function POST(request: NextRequest) {
audioFileName = file.name
audioMimeType = file.type
} else if (body.audioUrl) {
logger.info(`[${requestId}] Downloading from URL: ${body.audioUrl}`)
logger.info(`[${requestId}] Downloading from URL: ${sanitizeUrlForLog(body.audioUrl)}`)
const response = await fetch(body.audioUrl)
if (!response.ok) {

View File

@@ -94,6 +94,14 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Uploading document: ${userFile.name}`)
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
const filesOutput = [
{
name: userFile.name,
mimeType: userFile.type || 'application/octet-stream',
data: buffer.toString('base64'),
size: buffer.length,
},
]
logger.info(`[${requestId}] Downloaded file: ${buffer.length} bytes`)
@@ -136,6 +144,7 @@ export async function POST(request: NextRequest) {
output: {
message: 'Document sent successfully',
data: data.result,
files: filesOutput,
},
})
} catch (error) {

View File

@@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger'
import { ArrowDown, Loader2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/emcn'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { extractWorkspaceIdFromExecutionKey, getViewerUrl } from '@/lib/uploads/utils/file-utils'
const logger = createLogger('FileCards')
@@ -57,7 +58,7 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
if (file.key.startsWith('url/')) {
if (file.url) {
window.open(file.url, '_blank')
logger.info(`Opened URL-type file directly: ${file.url}`)
logger.info(`Opened URL-type file directly: ${sanitizeUrlForLog(file.url)}`)
return
}
throw new Error('URL is required for URL-type files')
@@ -77,13 +78,13 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
const serveUrl =
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
window.open(serveUrl, '_blank')
logger.info(`Opened execution file serve URL: ${serveUrl}`)
logger.info(`Opened execution file serve URL: ${sanitizeUrlForLog(serveUrl)}`)
} else {
const viewerUrl = resolvedWorkspaceId ? getViewerUrl(file.key, resolvedWorkspaceId) : null
if (viewerUrl) {
router.push(viewerUrl)
logger.info(`Navigated to viewer URL: ${viewerUrl}`)
logger.info(`Navigated to viewer URL: ${sanitizeUrlForLog(viewerUrl)}`)
} else {
logger.warn(
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`

View File

@@ -807,7 +807,7 @@ export function Chat() {
const newReservedFields: StartInputFormatField[] = missingStartReservedFields.map(
(fieldName) => {
const defaultType = fieldName === 'files' ? 'files' : 'string'
const defaultType = fieldName === 'files' ? 'file[]' : 'string'
return {
id: crypto.randomUUID(),

View File

@@ -225,7 +225,7 @@ const getOutputTypeForPath = (
const chatModeTypes: Record<string, string> = {
input: 'string',
conversationId: 'string',
files: 'files',
files: 'file[]',
}
return chatModeTypes[outputPath] || 'any'
}
@@ -1563,16 +1563,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
blockTagGroups.sort((a, b) => a.distance - b.distance)
finalBlockTagGroups.push(...blockTagGroups)
const contextualTags: string[] = []
if (loopBlockGroup) {
contextualTags.push(...loopBlockGroup.tags)
}
if (parallelBlockGroup) {
contextualTags.push(...parallelBlockGroup.tags)
}
const groupTags = finalBlockTagGroups.flatMap((group) => group.tags)
const tags = [...groupTags, ...variableTags]
return {
tags: [...allBlockTags, ...variableTags, ...contextualTags],
tags,
variableInfoMap,
blockTagGroups: finalBlockTagGroups,
}

View File

@@ -578,13 +578,20 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
if (!params.serverId) throw new Error('Server ID is required')
switch (params.operation) {
case 'discord_send_message':
case 'discord_send_message': {
const fileParam = params.attachmentFiles || params.files
const normalizedFiles = fileParam
? Array.isArray(fileParam)
? fileParam
: [fileParam]
: undefined
return {
...commonParams,
channelId: params.channelId,
content: params.content,
files: params.attachmentFiles || params.files,
files: normalizedFiles,
}
}
case 'discord_get_messages':
return {
...commonParams,
@@ -789,6 +796,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
},
outputs: {
message: { type: 'string', description: 'Status message' },
files: { type: 'file[]', description: 'Files attached to the message' },
data: { type: 'json', description: 'Response data' },
},
}

View File

@@ -73,5 +73,6 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
outputs: {
audioUrl: { type: 'string', description: 'Generated audio URL' },
audioFile: { type: 'file', description: 'Generated audio file' },
},
}

View File

@@ -34,6 +34,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
{ label: 'Update Comment', id: 'update_comment' },
{ label: 'Delete Comment', id: 'delete_comment' },
{ label: 'Get Attachments', id: 'get_attachments' },
{ label: 'Add Attachment', id: 'add_attachment' },
{ label: 'Delete Attachment', id: 'delete_attachment' },
{ label: 'Add Worklog', id: 'add_worklog' },
{ label: 'Get Worklogs', id: 'get_worklogs' },
@@ -137,6 +138,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
'update_comment',
'delete_comment',
'get_attachments',
'add_attachment',
'add_worklog',
'get_worklogs',
'update_worklog',
@@ -168,6 +170,7 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
'update_comment',
'delete_comment',
'get_attachments',
'add_attachment',
'add_worklog',
'get_worklogs',
'update_worklog',
@@ -407,6 +410,27 @@ Return ONLY the comment text - no explanations.`,
condition: { field: 'operation', value: ['update_comment', 'delete_comment'] },
},
// Attachment fields
{
id: 'attachmentFiles',
title: 'Attachments',
type: 'file-upload',
canonicalParamId: 'files',
placeholder: 'Upload files',
condition: { field: 'operation', value: 'add_attachment' },
mode: 'basic',
multiple: true,
required: true,
},
{
id: 'files',
title: 'File References',
type: 'short-input',
canonicalParamId: 'files',
placeholder: 'File reference from previous block',
condition: { field: 'operation', value: 'add_attachment' },
mode: 'advanced',
required: true,
},
{
id: 'attachmentId',
title: 'Attachment ID',
@@ -576,6 +600,7 @@ Return ONLY the comment text - no explanations.`,
'jira_update_comment',
'jira_delete_comment',
'jira_get_attachments',
'jira_add_attachment',
'jira_delete_attachment',
'jira_add_worklog',
'jira_get_worklogs',
@@ -623,6 +648,8 @@ Return ONLY the comment text - no explanations.`,
return 'jira_delete_comment'
case 'get_attachments':
return 'jira_get_attachments'
case 'add_attachment':
return 'jira_add_attachment'
case 'delete_attachment':
return 'jira_delete_attachment'
case 'add_worklog':
@@ -838,6 +865,21 @@ Return ONLY the comment text - no explanations.`,
issueKey: effectiveIssueKey,
}
}
case 'add_attachment': {
if (!effectiveIssueKey) {
throw new Error('Issue Key is required to add attachments.')
}
const fileParam = params.attachmentFiles || params.files
if (!fileParam || (Array.isArray(fileParam) && fileParam.length === 0)) {
throw new Error('At least one attachment file is required.')
}
const normalizedFiles = Array.isArray(fileParam) ? fileParam : [fileParam]
return {
...baseParams,
issueKey: effectiveIssueKey,
files: normalizedFiles,
}
}
case 'delete_attachment': {
return {
...baseParams,
@@ -982,6 +1024,8 @@ Return ONLY the comment text - no explanations.`,
commentBody: { type: 'string', description: 'Text content for comment operations' },
commentId: { type: 'string', description: 'Comment ID for update/delete operations' },
// Attachment operation inputs
attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' },
files: { type: 'array', description: 'Files to attach (UserFile array)' },
attachmentId: { type: 'string', description: 'Attachment ID for delete operation' },
// Worklog operation inputs
timeSpentSeconds: {
@@ -1049,9 +1093,11 @@ Return ONLY the comment text - no explanations.`,
// jira_get_attachments outputs
attachments: {
type: 'file[]',
type: 'json',
description: 'Array of attachments with id, filename, size, mimeType, created, author',
},
files: { type: 'file[]', description: 'Uploaded attachment files' },
attachmentIds: { type: 'json', description: 'Uploaded attachment IDs' },
// jira_delete_attachment, jira_delete_comment, jira_delete_issue, jira_delete_worklog, jira_delete_issue_link outputs
attachmentId: { type: 'string', description: 'Deleted attachment ID' },

View File

@@ -668,17 +668,44 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
generationType: 'timestamp',
},
},
// Attachment file
{
id: 'attachmentFileUpload',
title: 'Attachment',
type: 'file-upload',
canonicalParamId: 'file',
placeholder: 'Upload attachment',
condition: {
field: 'operation',
value: ['linear_create_attachment'],
},
mode: 'basic',
multiple: false,
},
{
id: 'file',
title: 'File Reference',
type: 'short-input',
canonicalParamId: 'file',
placeholder: 'File reference from previous block',
condition: {
field: 'operation',
value: ['linear_create_attachment'],
},
mode: 'advanced',
},
// Attachment URL
{
id: 'url',
title: 'URL',
type: 'short-input',
placeholder: 'Enter URL',
required: true,
required: false,
condition: {
field: 'operation',
value: ['linear_create_attachment'],
},
mode: 'advanced',
},
// Attachment title
{
@@ -1742,16 +1769,31 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
teamId: effectiveTeamId,
}
case 'linear_create_attachment':
if (!params.issueId?.trim() || !params.url?.trim()) {
throw new Error('Issue ID and URL are required.')
case 'linear_create_attachment': {
if (!params.issueId?.trim()) {
throw new Error('Issue ID is required.')
}
if (Array.isArray(params.file)) {
throw new Error('Attachment file must be a single file.')
}
if (Array.isArray(params.attachmentFileUpload)) {
throw new Error('Attachment file must be a single file.')
}
const attachmentFile = params.attachmentFileUpload || params.file
const attachmentUrl =
params.url?.trim() ||
(attachmentFile && !Array.isArray(attachmentFile) ? attachmentFile.url : undefined)
if (!attachmentUrl) {
throw new Error('URL or file is required.')
}
return {
...baseParams,
issueId: params.issueId.trim(),
url: params.url.trim(),
url: attachmentUrl,
file: attachmentFile,
title: params.attachmentTitle,
}
}
case 'linear_list_attachments':
if (!params.issueId?.trim()) {
@@ -2248,6 +2290,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
endDate: { type: 'string', description: 'End date' },
targetDate: { type: 'string', description: 'Target date' },
url: { type: 'string', description: 'URL' },
attachmentFileUpload: { type: 'json', description: 'File to attach (UI upload)' },
file: { type: 'json', description: 'File to attach (UserFile)' },
attachmentTitle: { type: 'string', description: 'Attachment title' },
attachmentId: { type: 'string', description: 'Attachment identifier' },
relationType: { type: 'string', description: 'Relation type' },
@@ -2341,7 +2385,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
cycles: { type: 'json', description: 'Cycles list' },
// Attachment outputs
attachment: { type: 'json', description: 'Attachment data' },
attachments: { type: 'file[]', description: 'Attachments list' },
attachments: { type: 'json', description: 'Attachments list' },
// Relation outputs
relation: { type: 'json', description: 'Issue relation data' },
relations: { type: 'json', description: 'Issue relations list' },

View File

@@ -346,7 +346,10 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
// Add files if provided
const fileParam = attachmentFiles || files
if (fileParam && (operation === 'write_chat' || operation === 'write_channel')) {
baseParams.files = fileParam
const normalizedFiles = Array.isArray(fileParam) ? fileParam : [fileParam]
if (normalizedFiles.length > 0) {
baseParams.files = normalizedFiles
}
}
// Add messageId if provided
@@ -463,6 +466,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
totalAttachments: { type: 'number', description: 'Total number of attachments' },
attachmentTypes: { type: 'json', description: 'Array of attachment content types' },
attachments: { type: 'file[]', description: 'Downloaded message attachments' },
files: { type: 'file[]', description: 'Files attached to the message' },
updatedContent: {
type: 'boolean',
description: 'Whether content was successfully updated/sent',

View File

@@ -803,7 +803,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
outputs: {
deals: { type: 'json', description: 'Array of deal objects' },
deal: { type: 'json', description: 'Single deal object' },
files: { type: 'file[]', description: 'Array of file objects' },
files: { type: 'json', description: 'Array of file objects' },
messages: { type: 'json', description: 'Array of mail message objects' },
pipelines: { type: 'json', description: 'Array of pipeline objects' },
projects: { type: 'json', description: 'Array of project objects' },

View File

@@ -293,6 +293,7 @@ export const SftpBlock: BlockConfig<SftpUploadResult> = {
outputs: {
success: { type: 'boolean', description: 'Whether the operation was successful' },
uploadedFiles: { type: 'json', description: 'Array of uploaded file details' },
file: { type: 'file', description: 'Downloaded file stored in execution files' },
fileName: { type: 'string', description: 'Downloaded file name' },
content: { type: 'string', description: 'Downloaded file content' },
size: { type: 'number', description: 'File size in bytes' },

View File

@@ -522,7 +522,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
description: 'Array of SharePoint list items with fields',
},
uploadedFiles: {
type: 'file[]',
type: 'json',
description: 'Array of uploaded file objects with id, name, webUrl, size',
},
fileCount: {

View File

@@ -622,7 +622,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
}
const fileParam = attachmentFiles || files
if (fileParam) {
baseParams.files = fileParam
const normalizedFiles = Array.isArray(fileParam) ? fileParam : [fileParam]
if (normalizedFiles.length > 0) {
baseParams.files = normalizedFiles
}
}
break
}
@@ -796,6 +799,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
type: 'number',
description: 'Number of files uploaded (when files are attached)',
},
files: { type: 'file[]', description: 'Files attached to the message' },
// slack_canvas outputs
canvas_id: { type: 'string', description: 'Canvas identifier for created canvases' },

View File

@@ -507,6 +507,7 @@ export const SSHBlock: BlockConfig<SSHResponse> = {
stderr: { type: 'string', description: 'Command standard error' },
exitCode: { type: 'number', description: 'Command exit code' },
success: { type: 'boolean', description: 'Operation success status' },
file: { type: 'file', description: 'Downloaded file stored in execution files' },
fileContent: { type: 'string', description: 'Downloaded/read file content' },
entries: { type: 'json', description: 'Directory entries' },
exists: { type: 'boolean', description: 'File/directory existence' },

View File

@@ -314,9 +314,14 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
case 'telegram_send_document': {
// Handle file upload
const fileParam = params.attachmentFiles || params.files
const normalizedFiles = fileParam
? Array.isArray(fileParam)
? fileParam
: [fileParam]
: undefined
return {
...commonParams,
files: fileParam,
files: normalizedFiles,
caption: params.caption,
}
}
@@ -359,6 +364,7 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
},
message: { type: 'string', description: 'Success or error message' },
data: { type: 'json', description: 'Response data' },
files: { type: 'file[]', description: 'Files attached to the message' },
// Specific result fields
messageId: { type: 'number', description: 'Sent message ID' },
chatId: { type: 'number', description: 'Chat ID where message was sent' },

View File

@@ -0,0 +1,19 @@
/**
* Sanitize URLs for logging by stripping query/hash and truncating.
*/
export function sanitizeUrlForLog(url: string, maxLength = 120): string {
if (!url) return ''
const trimmed = url.trim()
try {
const hasProtocol = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed)
const parsed = new URL(trimmed, hasProtocol ? undefined : 'http://localhost')
const origin = parsed.origin === 'null' ? '' : parsed.origin
const sanitized = `${origin}${parsed.pathname}`
const result = sanitized || parsed.pathname || trimmed
return result.length > maxLength ? `${result.slice(0, maxLength)}...` : result
} catch {
const withoutQuery = trimmed.split('?')[0].split('#')[0]
return withoutQuery.length > maxLength ? `${withoutQuery.slice(0, maxLength)}...` : withoutQuery
}
}

View File

@@ -3,6 +3,7 @@ import { PDFDocument } from 'pdf-lib'
import { getBYOKKey } from '@/lib/api-key/byok'
import { type Chunk, JsonYamlChunker, StructuredDataChunker, TextChunker } from '@/lib/chunkers'
import { env } from '@/lib/core/config/env'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { parseBuffer, parseFile } from '@/lib/file-parsers'
import type { FileParseMetadata } from '@/lib/file-parsers/types'
import { retryWithExponentialBackoff } from '@/lib/knowledge/documents/utils'
@@ -489,7 +490,7 @@ async function parseWithMistralOCR(
workspaceId
)
logger.info(`Mistral OCR: Using presigned URL for ${filename}: ${httpsUrl.substring(0, 120)}...`)
logger.info(`Mistral OCR: Using presigned URL for ${filename}: ${sanitizeUrlForLog(httpsUrl)}`)
let pageCount = 0
if (mimeType === 'application/pdf' && buffer) {

View File

@@ -11,6 +11,7 @@ import {
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import type { DbOrTx } from '@/lib/db/types'
import { getProviderIdFromServiceId } from '@/lib/oauth'
import {
@@ -114,7 +115,7 @@ async function fetchWithDNSPinning(
const urlValidation = await validateUrlWithDNS(url, 'contentUrl')
if (!urlValidation.isValid) {
logger.warn(`[${requestId}] Invalid content URL: ${urlValidation.error}`, {
url: url.substring(0, 100),
url: sanitizeUrlForLog(url),
})
return null
}
@@ -133,7 +134,7 @@ async function fetchWithDNSPinning(
} catch (error) {
logger.error(`[${requestId}] Error fetching URL with DNS pinning`, {
error: error instanceof Error ? error.message : String(error),
url: url.substring(0, 100),
url: sanitizeUrlForLog(url),
})
return null
}

View File

@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import type { BrowserUseRunTaskParams, BrowserUseRunTaskResponse } from '@/tools/browser_use/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
@@ -183,7 +184,7 @@ async function pollForCompletion(
}
if (!liveUrlLogged && taskData.live_url) {
logger.info(`BrowserUse task ${taskId} live URL: ${taskData.live_url}`)
logger.info(`BrowserUse task ${taskId} live URL: ${sanitizeUrlForLog(taskData.live_url)}`)
liveUrlLogged = true
}

View File

@@ -72,6 +72,7 @@ export const discordSendMessageTool: ToolConfig<
outputs: {
message: { type: 'string', description: 'Success or error message' },
files: { type: 'file[]', description: 'Files attached to the message' },
data: {
type: 'object',
description: 'Discord message data',

View File

@@ -1,4 +1,5 @@
import type { UserFile } from '@/executor/types'
import type { ToolFileData } from '@/tools/types'
export interface DiscordMessage {
id: string
@@ -85,6 +86,7 @@ interface BaseDiscordResponse {
export interface DiscordSendMessageResponse extends BaseDiscordResponse {
output: {
message: string
files?: ToolFileData[]
data?: DiscordMessage
}
}

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
export interface ElevenLabsTtsParams {
@@ -12,11 +13,13 @@ export interface ElevenLabsTtsParams {
export interface ElevenLabsTtsResponse extends ToolResponse {
output: {
audioUrl: string
audioFile?: UserFile
}
}
export interface ElevenLabsBlockResponse extends ToolResponse {
output: {
audioUrl: string
audioFile?: UserFile
}
}

View File

@@ -0,0 +1,83 @@
import type { JiraAddAttachmentParams, JiraAddAttachmentResponse } from '@/tools/jira/types'
import type { ToolConfig } from '@/tools/types'
export const jiraAddAttachmentTool: ToolConfig<JiraAddAttachmentParams, JiraAddAttachmentResponse> =
{
id: 'jira_add_attachment',
name: 'Jira Add Attachment',
description: 'Add attachments to a Jira issue',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
issueKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Jira issue key to add attachments to (e.g., PROJ-123)',
},
files: {
type: 'file[]',
required: true,
visibility: 'user-only',
description: 'Files to attach to the Jira issue',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description:
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: '/api/tools/jira/add-attachment',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: JiraAddAttachmentParams) => ({
accessToken: params.accessToken,
domain: params.domain,
issueKey: params.issueKey,
files: params.files,
cloudId: params.cloudId,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok || !data.success) {
throw new Error(data.error || 'Failed to add Jira attachment')
}
return {
success: true,
output: data.output,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
issueKey: { type: 'string', description: 'Issue key' },
attachmentIds: { type: 'json', description: 'IDs of uploaded attachments' },
files: { type: 'file[]', description: 'Uploaded attachment files' },
},
}

View File

@@ -1,3 +1,4 @@
import { jiraAddAttachmentTool } from '@/tools/jira/add_attachment'
import { jiraAddCommentTool } from '@/tools/jira/add_comment'
import { jiraAddWatcherTool } from '@/tools/jira/add_watcher'
import { jiraAddWorklogTool } from '@/tools/jira/add_worklog'
@@ -32,6 +33,7 @@ export {
jiraTransitionIssueTool,
jiraSearchIssuesTool,
jiraAddCommentTool,
jiraAddAttachmentTool,
jiraGetCommentsTool,
jiraUpdateCommentTool,
jiraDeleteCommentTool,

View File

@@ -1,4 +1,5 @@
import type { ToolResponse } from '@/tools/types'
import type { UserFile } from '@/executor/types'
import type { ToolFileData, ToolResponse } from '@/tools/types'
export interface JiraRetrieveParams {
accessToken: string
@@ -312,6 +313,23 @@ export interface JiraDeleteAttachmentResponse extends ToolResponse {
}
}
export interface JiraAddAttachmentParams {
accessToken: string
domain: string
issueKey: string
files: UserFile[]
cloudId?: string
}
export interface JiraAddAttachmentResponse extends ToolResponse {
output: {
ts: string
issueKey: string
attachmentIds: string[]
files: ToolFileData[]
}
}
// Worklogs
export interface JiraAddWorklogParams {
accessToken: string
@@ -482,6 +500,7 @@ export type JiraResponse =
| JiraUpdateCommentResponse
| JiraDeleteCommentResponse
| JiraGetAttachmentsResponse
| JiraAddAttachmentResponse
| JiraDeleteAttachmentResponse
| JiraAddWorklogResponse
| JiraGetWorklogsResponse

View File

@@ -28,10 +28,16 @@ export const linearCreateAttachmentTool: ToolConfig<
},
url: {
type: 'string',
required: true,
required: false,
visibility: 'user-or-llm',
description: 'URL of the attachment',
},
file: {
type: 'file',
required: false,
visibility: 'user-only',
description: 'File to attach',
},
title: {
type: 'string',
required: true,
@@ -59,9 +65,14 @@ export const linearCreateAttachmentTool: ToolConfig<
}
},
body: (params) => {
const attachmentUrl = params.url || params.file?.url
if (!attachmentUrl) {
throw new Error('URL or file is required')
}
const input: Record<string, any> = {
issueId: params.issueId,
url: params.url,
url: attachmentUrl,
title: params.title,
}

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
@@ -875,7 +876,8 @@ export interface LinearGetActiveCycleParams {
export interface LinearCreateAttachmentParams {
issueId: string
url: string
url?: string
file?: UserFile
title?: string
subtitle?: string
accessToken?: string

View File

@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import type {
MicrosoftPlannerReadResponse,
MicrosoftPlannerToolParams,
@@ -76,7 +77,7 @@ export const readTaskTool: ToolConfig<MicrosoftPlannerToolParams, MicrosoftPlann
finalUrl = 'https://graph.microsoft.com/v1.0/me/planner/tasks'
}
logger.info('Microsoft Planner URL:', finalUrl)
logger.info('Microsoft Planner URL:', sanitizeUrlForLog(finalUrl))
return finalUrl
},
method: 'GET',

View File

@@ -1,5 +1,5 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
import type { ToolFileData, ToolResponse } from '@/tools/types'
export interface MicrosoftTeamsAttachment {
id: string
@@ -61,6 +61,7 @@ export interface MicrosoftTeamsWriteResponse extends ToolResponse {
output: {
updatedContent: boolean
metadata: MicrosoftTeamsMetadata
files?: ToolFileData[]
}
}

View File

@@ -58,6 +58,7 @@ export const writeChannelTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTea
createdTime: { type: 'string', description: 'Timestamp when message was created' },
url: { type: 'string', description: 'Web URL to the message' },
updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' },
files: { type: 'file[]', description: 'Files attached to the message' },
},
request: {

View File

@@ -50,6 +50,7 @@ export const writeChatTool: ToolConfig<MicrosoftTeamsToolParams, MicrosoftTeamsW
createdTime: { type: 'string', description: 'Timestamp when message was created' },
url: { type: 'string', description: 'Web URL to the message' },
updatedContent: { type: 'boolean', description: 'Whether content was successfully updated' },
files: { type: 'file[]', description: 'Files attached to the message' },
},
request: {

View File

@@ -98,10 +98,10 @@ export const mistralParserTool: ToolConfig<MistralParserInput, MistralParserOutp
if (
typeof params.fileUpload === 'object' &&
params.fileUpload !== null &&
(params.fileUpload.url || params.fileUpload.path)
params.fileUpload.url
) {
// Get the full URL to the file - prefer url over path for UserFile compatibility
let uploadedFilePath = params.fileUpload.url || params.fileUpload.path
// Get the full URL to the file
let uploadedFilePath = params.fileUpload.url
// Make sure the file path is an absolute URL
if (uploadedFilePath.startsWith('/')) {
@@ -184,9 +184,9 @@ export const mistralParserTool: ToolConfig<MistralParserInput, MistralParserOutp
}
// Check if this is an internal workspace file path
if (params.fileUpload?.path?.startsWith('/api/files/serve/')) {
if (params.fileUpload?.url?.startsWith('/api/files/serve/')) {
// Update filePath to the internal path for workspace files
requestBody.filePath = params.fileUpload.path
requestBody.filePath = params.fileUpload.url
}
// Add optional parameters with proper validation

View File

@@ -763,6 +763,7 @@ import {
} from '@/tools/intercom'
import { jinaReadUrlTool, jinaSearchTool } from '@/tools/jina'
import {
jiraAddAttachmentTool,
jiraAddCommentTool,
jiraAddWatcherTool,
jiraAddWorklogTool,
@@ -1879,6 +1880,7 @@ export const tools: Record<string, ToolConfig> = {
jira_update_comment: jiraUpdateCommentTool,
jira_delete_comment: jiraDeleteCommentTool,
jira_get_attachments: jiraGetAttachmentsTool,
jira_add_attachment: jiraAddAttachmentTool,
jira_delete_attachment: jiraDeleteAttachmentTool,
jira_add_worklog: jiraAddWorklogTool,
jira_get_worklogs: jiraGetWorklogsTool,

View File

@@ -94,6 +94,7 @@ export const sftpDownloadTool: ToolConfig<SftpDownloadParams, SftpDownloadResult
output: {
success: true,
fileName: data.fileName,
file: data.file,
content: data.content,
size: data.size,
encoding: data.encoding,
@@ -104,6 +105,7 @@ export const sftpDownloadTool: ToolConfig<SftpDownloadParams, SftpDownloadResult
outputs: {
success: { type: 'boolean', description: 'Whether the download was successful' },
file: { type: 'file', description: 'Downloaded file stored in execution files' },
fileName: { type: 'string', description: 'Name of the downloaded file' },
content: { type: 'string', description: 'File content (text or base64 encoded)' },
size: { type: 'number', description: 'File size in bytes' },

View File

@@ -1,5 +1,5 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
import type { ToolFileData, ToolResponse } from '@/tools/types'
export interface SftpConnectionConfig {
host: string
@@ -42,6 +42,7 @@ export interface SftpDownloadResult extends ToolResponse {
output: {
success: boolean
fileName?: string
file?: ToolFileData
content?: string
size?: number
encoding?: string

View File

@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import type {
SharepointGetListResponse,
SharepointList,
@@ -56,7 +57,10 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
const baseUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists`
const url = new URL(baseUrl)
const finalUrl = url.toString()
logger.info('SharePoint List All Lists URL', { finalUrl, siteId })
logger.info('SharePoint List All Lists URL', {
finalUrl: sanitizeUrlForLog(finalUrl),
siteId,
})
return finalUrl
}
@@ -72,7 +76,7 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
itemsUrl.searchParams.set('$expand', 'fields')
const finalItemsUrl = itemsUrl.toString()
logger.info('SharePoint Get List Items URL', {
finalUrl: finalItemsUrl,
finalUrl: sanitizeUrlForLog(finalItemsUrl),
siteId,
listId: params.listId,
})
@@ -89,7 +93,7 @@ export const getListTool: ToolConfig<SharepointToolParams, SharepointGetListResp
const finalUrl = url.toString()
logger.info('SharePoint Get List URL', {
finalUrl,
finalUrl: sanitizeUrlForLog(finalUrl),
siteId,
listId: params.listId,
includeColumns: !!params.includeColumns,

View File

@@ -114,5 +114,6 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
type: 'number',
description: 'Number of files uploaded (when files are attached)',
},
files: { type: 'file[]', description: 'Files attached to the message' },
},
}

View File

@@ -1,5 +1,5 @@
import type { UserFile } from '@/executor/types'
import type { OutputProperty, ToolResponse } from '@/tools/types'
import type { OutputProperty, ToolFileData, ToolResponse } from '@/tools/types'
/**
* Shared output property definitions for Slack API responses.
@@ -596,6 +596,7 @@ export interface SlackMessageResponse extends ToolResponse {
ts: string
channel: string
fileCount?: number
files?: ToolFileData[]
// New comprehensive message object
message: SlackMessage
}

View File

@@ -80,6 +80,7 @@ export const downloadFileTool: ToolConfig<SSHDownloadFileParams, SSHResponse> =
success: true,
output: {
downloaded: true,
file: data.file,
fileContent: data.content,
fileName: data.fileName,
remotePath: data.remotePath,
@@ -91,6 +92,7 @@ export const downloadFileTool: ToolConfig<SSHDownloadFileParams, SSHResponse> =
outputs: {
downloaded: { type: 'boolean', description: 'Whether the file was downloaded successfully' },
file: { type: 'file', description: 'Downloaded file stored in execution files' },
fileContent: { type: 'string', description: 'File content (base64 encoded for binary files)' },
fileName: { type: 'string', description: 'Name of the downloaded file' },
remotePath: { type: 'string', description: 'Source path on the remote server' },

View File

@@ -1,4 +1,4 @@
import type { ToolResponse } from '@/tools/types'
import type { ToolFileData, ToolResponse } from '@/tools/types'
// Base SSH connection configuration
export interface SSHConnectionConfig {
@@ -149,6 +149,7 @@ export interface SSHResponse extends ToolResponse {
uploaded?: boolean
downloaded?: boolean
file?: ToolFileData
fileContent?: string
fileName?: string
remotePath?: string

View File

@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import type { StagehandAgentParams, StagehandAgentResponse } from '@/tools/stagehand/types'
import { STAGEHAND_AGENT_RESULT_OUTPUT_PROPERTIES } from '@/tools/stagehand/types'
import type { ToolConfig } from '@/tools/types'
@@ -61,7 +62,9 @@ export const agentTool: ToolConfig<StagehandAgentParams, StagehandAgentResponse>
let startUrl = params.startUrl
if (startUrl && !startUrl.match(/^https?:\/\//i)) {
startUrl = `https://${startUrl.trim()}`
logger.info(`Normalized URL from ${params.startUrl} to ${startUrl}`)
logger.info(
`Normalized URL from ${sanitizeUrlForLog(params.startUrl)} to ${sanitizeUrlForLog(startUrl)}`
)
}
return {

View File

@@ -1,3 +1,4 @@
import type { UserFile } from '@/executor/types'
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
@@ -441,7 +442,7 @@ export interface SupabaseStorageUploadParams {
bucket: string
fileName: string
path?: string
fileData: any // UserFile object (basic mode) or string (advanced mode: base64/plain text)
fileData: UserFile | string
contentType?: string
upsert?: boolean
}

View File

@@ -75,6 +75,7 @@ export const telegramSendDocumentTool: ToolConfig<
outputs: {
message: { type: 'string', description: 'Success or error message' },
files: { type: 'file[]', description: 'Files attached to the message' },
data: {
type: 'object',
description: 'Telegram message data including document',

View File

@@ -1,5 +1,5 @@
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
import type { ToolFileData, ToolResponse } from '@/tools/types'
export interface TelegramMessage {
message_id: number
@@ -167,6 +167,7 @@ export interface TelegramSendDocumentResponse extends ToolResponse {
output: {
message: string
data?: TelegramMedia
files?: ToolFileData[]
}
}

View File

@@ -1,4 +1,5 @@
// Common types for WordPress REST API tools
import type { UserFile } from '@/executor/types'
import type { ToolResponse } from '@/tools/types'
// Common parameters for all WordPress tools (WordPress.com OAuth)
@@ -254,7 +255,7 @@ export interface WordPressListPagesResponse extends ToolResponse {
// Upload Media
export interface WordPressUploadMediaParams extends WordPressBaseParams {
file: any // UserFile object from file upload
file: UserFile
filename?: string // Optional filename override
title?: string
caption?: string

View File

@@ -98,7 +98,7 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = {
},
message: {
raw: {
attachments: { type: 'file[]', description: 'Array of attachments' },
attachments: { type: 'json', description: 'Array of attachments' },
channelData: {
team: { id: { type: 'string', description: 'Team ID' } },
tenant: { id: { type: 'string', description: 'Tenant ID' } },