cleanup canonical tool param resolution code

This commit is contained in:
Vikhyath Mondreti
2026-02-04 22:26:06 -08:00
parent a02102345e
commit 994224322a
49 changed files with 822 additions and 540 deletions

View File

@@ -206,10 +206,15 @@ export const {Service}Block: BlockConfig = {
}
```
**Critical:**
- `canonicalParamId` must NOT match any other subblock's `id`, must be unique per block, and should only be used to link basic/advanced alternatives for the same parameter.
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent.
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions.
**Critical Canonical Param Rules:**
- `canonicalParamId` must NOT match any subblock's `id` in the block
- `canonicalParamId` must be unique per operation/condition context
- Only use `canonicalParamId` to link basic/advanced alternatives for the same logical parameter
- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent
- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions
- **Required consistency:** If one subblock in a canonical group has `required: true`, ALL subblocks in that group must have `required: true` (prevents bypassing validation by switching modes)
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs (e.g., `fileSelector`, `manualFileId`)
- **Params function:** Must use canonical param IDs, NOT raw subblock IDs (raw IDs are deleted after canonical transformation)
## Step 4: Add Icon

View File

@@ -157,6 +157,36 @@ dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
- `'both'` - Show in both modes (default)
- `'trigger'` - Only when block is used as trigger
### `canonicalParamId` - Link basic/advanced alternatives
Use to map multiple UI inputs to a single logical parameter:
```typescript
// Basic mode: Visual selector
{
id: 'fileSelector',
type: 'file-selector',
mode: 'basic',
canonicalParamId: 'fileId',
required: true,
},
// Advanced mode: Manual input
{
id: 'manualFileId',
type: 'short-input',
mode: 'advanced',
canonicalParamId: 'fileId',
required: true,
},
```
**Critical Rules:**
- `canonicalParamId` must NOT match any subblock's `id`
- `canonicalParamId` must be unique per operation/condition context
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
**Register in `blocks/registry.ts`:**
```typescript

View File

@@ -155,6 +155,36 @@ dependsOn: { all: ['authMethod'], any: ['credential', 'botToken'] }
- `'both'` - Show in both modes (default)
- `'trigger'` - Only when block is used as trigger
### `canonicalParamId` - Link basic/advanced alternatives
Use to map multiple UI inputs to a single logical parameter:
```typescript
// Basic mode: Visual selector
{
id: 'fileSelector',
type: 'file-selector',
mode: 'basic',
canonicalParamId: 'fileId',
required: true,
},
// Advanced mode: Manual input
{
id: 'manualFileId',
type: 'short-input',
mode: 'advanced',
canonicalParamId: 'fileId',
required: true,
},
```
**Critical Rules:**
- `canonicalParamId` must NOT match any subblock's `id`
- `canonicalParamId` must be unique per operation/condition context
- **Required consistency:** All subblocks in a canonical group must have the same `required` status
- **Inputs section:** Must list canonical param IDs (e.g., `fileId`), NOT raw subblock IDs
- **Params function:** Must use canonical param IDs (raw IDs are deleted after canonical transformation)
**Register in `blocks/registry.ts`:**
```typescript

View File

@@ -649,4 +649,394 @@ describe('Blocks Module', () => {
}
})
})
describe('Canonical Param Validation', () => {
/**
* Helper to serialize a condition for comparison
*/
function serializeCondition(condition: unknown): string {
if (!condition) return ''
return JSON.stringify(condition)
}
it('should not have canonicalParamId that matches any subBlock id within the same block', () => {
const blocks = getAllBlocks()
const errors: string[] = []
for (const block of blocks) {
const allSubBlockIds = new Set(block.subBlocks.map((sb) => sb.id))
const canonicalParamIds = new Set(
block.subBlocks.filter((sb) => sb.canonicalParamId).map((sb) => sb.canonicalParamId)
)
for (const canonicalId of canonicalParamIds) {
if (allSubBlockIds.has(canonicalId!)) {
// Check if the matching subBlock also has a canonicalParamId pointing to itself
const matchingSubBlock = block.subBlocks.find(
(sb) => sb.id === canonicalId && !sb.canonicalParamId
)
if (matchingSubBlock) {
errors.push(
`Block "${block.type}": canonicalParamId "${canonicalId}" clashes with subBlock id "${canonicalId}"`
)
}
}
}
}
if (errors.length > 0) {
throw new Error(`Canonical param ID clashes detected:\n${errors.join('\n')}`)
}
})
it('should have unique subBlock IDs within the same condition context', () => {
const blocks = getAllBlocks()
const errors: string[] = []
for (const block of blocks) {
// Group subBlocks by their condition (only for static/JSON conditions, not functions)
const subBlocksByCondition = new Map<
string,
Array<{ id: string; mode?: string; hasCanonical: boolean }>
>()
for (const subBlock of block.subBlocks) {
// Skip subBlocks with function conditions - we can't evaluate them statically
// These are valid when the function returns different conditions at runtime
if (typeof subBlock.condition === 'function') {
continue
}
const conditionKey = serializeCondition(subBlock.condition)
if (!subBlocksByCondition.has(conditionKey)) {
subBlocksByCondition.set(conditionKey, [])
}
subBlocksByCondition.get(conditionKey)!.push({
id: subBlock.id,
mode: subBlock.mode,
hasCanonical: Boolean(subBlock.canonicalParamId),
})
}
// Check for duplicate IDs within the same condition (excluding canonical pairs and mode swaps)
for (const [conditionKey, subBlocks] of subBlocksByCondition) {
const idCounts = new Map<string, number>()
for (const sb of subBlocks) {
idCounts.set(sb.id, (idCounts.get(sb.id) || 0) + 1)
}
for (const [id, count] of idCounts) {
if (count > 1) {
const duplicates = subBlocks.filter((sb) => sb.id === id)
// Categorize modes
const basicModes = duplicates.filter(
(sb) => !sb.mode || sb.mode === 'basic' || sb.mode === 'both'
)
const advancedModes = duplicates.filter((sb) => sb.mode === 'advanced')
const triggerModes = duplicates.filter((sb) => sb.mode === 'trigger')
// Valid pattern 1: basic/advanced mode swap (with or without canonicalParamId)
if (
basicModes.length === 1 &&
advancedModes.length === 1 &&
triggerModes.length === 0
) {
continue // This is a valid basic/advanced mode swap pair
}
// Valid pattern 2: basic/trigger mode separation (trigger version for trigger mode)
// One basic/both + one or more trigger versions is valid
if (
basicModes.length <= 1 &&
advancedModes.length === 0 &&
triggerModes.length >= 1
) {
continue // This is a valid pattern where trigger mode has its own subBlock
}
// Valid pattern 3: All duplicates have canonicalParamId (they form a canonical group)
const allHaveCanonical = duplicates.every((sb) => sb.hasCanonical)
if (allHaveCanonical) {
continue // Validated separately by canonical pair tests
}
// Invalid: duplicates without proper pairing
const condition = conditionKey || '(no condition)'
const modeBreakdown = duplicates.map((d) => d.mode || 'basic/both').join(', ')
errors.push(
`Block "${block.type}": Duplicate subBlock id "${id}" with condition ${condition} (count: ${count}, modes: ${modeBreakdown})`
)
}
}
}
}
if (errors.length > 0) {
throw new Error(`Duplicate subBlock IDs detected:\n${errors.join('\n')}`)
}
})
it('should have properly formed canonical pairs (matching conditions)', () => {
const blocks = getAllBlocks()
const errors: string[] = []
for (const block of blocks) {
// Group subBlocks by canonicalParamId
const canonicalGroups = new Map<
string,
Array<{ id: string; mode?: string; condition: unknown; isStaticCondition: boolean }>
>()
for (const subBlock of block.subBlocks) {
if (subBlock.canonicalParamId) {
if (!canonicalGroups.has(subBlock.canonicalParamId)) {
canonicalGroups.set(subBlock.canonicalParamId, [])
}
canonicalGroups.get(subBlock.canonicalParamId)!.push({
id: subBlock.id,
mode: subBlock.mode,
condition: subBlock.condition,
isStaticCondition: typeof subBlock.condition !== 'function',
})
}
}
// Validate each canonical group
for (const [canonicalId, members] of canonicalGroups) {
// Only validate condition matching for static conditions
const staticMembers = members.filter((m) => m.isStaticCondition)
if (staticMembers.length > 1) {
const conditions = staticMembers.map((m) => serializeCondition(m.condition))
const uniqueConditions = new Set(conditions)
if (uniqueConditions.size > 1) {
errors.push(
`Block "${block.type}": Canonical param "${canonicalId}" has members with different conditions: ${[...uniqueConditions].join(' vs ')}`
)
}
}
// Check for proper basic/advanced pairing
const basicMembers = members.filter((m) => !m.mode || m.mode === 'basic')
const advancedMembers = members.filter((m) => m.mode === 'advanced')
if (basicMembers.length > 1) {
errors.push(
`Block "${block.type}": Canonical param "${canonicalId}" has ${basicMembers.length} basic mode members (should have at most 1)`
)
}
if (basicMembers.length === 0 && advancedMembers.length === 0) {
errors.push(
`Block "${block.type}": Canonical param "${canonicalId}" has no basic or advanced mode members`
)
}
}
}
if (errors.length > 0) {
throw new Error(`Canonical pair validation errors:\n${errors.join('\n')}`)
}
})
it('should have unique canonicalParamIds per operation/condition context', () => {
const blocks = getAllBlocks()
const errors: string[] = []
for (const block of blocks) {
// Group by condition + canonicalParamId to detect same canonical used for different operations
const canonicalByCondition = new Map<string, Set<string>>()
for (const subBlock of block.subBlocks) {
if (subBlock.canonicalParamId) {
// Skip function conditions - we can't evaluate them statically
if (typeof subBlock.condition === 'function') {
continue
}
const conditionKey = serializeCondition(subBlock.condition)
if (!canonicalByCondition.has(subBlock.canonicalParamId)) {
canonicalByCondition.set(subBlock.canonicalParamId, new Set())
}
canonicalByCondition.get(subBlock.canonicalParamId)!.add(conditionKey)
}
}
// Check that each canonicalParamId is only used for one condition
for (const [canonicalId, conditions] of canonicalByCondition) {
if (conditions.size > 1) {
errors.push(
`Block "${block.type}": Canonical param "${canonicalId}" is used across ${conditions.size} different conditions. Each operation should have its own unique canonicalParamId.`
)
}
}
}
if (errors.length > 0) {
throw new Error(`Canonical param reuse across conditions:\n${errors.join('\n')}`)
}
})
it('should have inputs containing canonical param IDs instead of raw subBlock IDs', () => {
const blocks = getAllBlocks()
const errors: string[] = []
for (const block of blocks) {
if (!block.inputs) continue
// Find all canonical groups (subBlocks with canonicalParamId)
const canonicalGroups = new Map<string, string[]>()
for (const subBlock of block.subBlocks) {
if (subBlock.canonicalParamId) {
if (!canonicalGroups.has(subBlock.canonicalParamId)) {
canonicalGroups.set(subBlock.canonicalParamId, [])
}
canonicalGroups.get(subBlock.canonicalParamId)!.push(subBlock.id)
}
}
const inputKeys = Object.keys(block.inputs)
for (const [canonicalId, rawSubBlockIds] of canonicalGroups) {
// Check that the canonical param ID is in inputs
if (!inputKeys.includes(canonicalId)) {
errors.push(
`Block "${block.type}": inputs section is missing canonical param "${canonicalId}"`
)
}
// Check that raw subBlock IDs are NOT in inputs (they get deleted after transformation)
for (const rawId of rawSubBlockIds) {
if (rawId !== canonicalId && inputKeys.includes(rawId)) {
errors.push(
`Block "${block.type}": inputs section contains raw subBlock id "${rawId}" which should be replaced by canonical param "${canonicalId}"`
)
}
}
}
}
if (errors.length > 0) {
throw new Error(`Inputs section validation errors:\n${errors.join('\n')}`)
}
})
it('should have params function using canonical IDs instead of raw subBlock IDs', () => {
const blocks = getAllBlocks()
const errors: string[] = []
for (const block of blocks) {
// Check if block has a params function
const paramsFunc = block.tools?.config?.params
if (!paramsFunc || typeof paramsFunc !== 'function') continue
// Get the function source code, stripping comments to avoid false positives
const rawFuncSource = paramsFunc.toString()
// Remove single-line comments (// ...) and multi-line comments (/* ... */)
const funcSource = rawFuncSource
.replace(/\/\/[^\n]*/g, '') // Remove single-line comments
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
// Find all canonical groups (subBlocks with canonicalParamId)
const canonicalGroups = new Map<string, string[]>()
for (const subBlock of block.subBlocks) {
if (subBlock.canonicalParamId) {
if (!canonicalGroups.has(subBlock.canonicalParamId)) {
canonicalGroups.set(subBlock.canonicalParamId, [])
}
canonicalGroups.get(subBlock.canonicalParamId)!.push(subBlock.id)
}
}
// Check for raw subBlock IDs being used in the params function
for (const [canonicalId, rawSubBlockIds] of canonicalGroups) {
for (const rawId of rawSubBlockIds) {
// Skip if the rawId is the same as the canonicalId (self-referential, which is allowed in some cases)
if (rawId === canonicalId) continue
// Check if the params function references the raw subBlock ID
// Look for patterns like: params.rawId, { rawId }, destructuring rawId
const patterns = [
new RegExp(`params\\.${rawId}\\b`), // params.rawId
new RegExp(`\\{[^}]*\\b${rawId}\\b[^}]*\\}\\s*=\\s*params`), // { rawId } = params
new RegExp(`\\b${rawId}\\s*[,}]`), // rawId in destructuring
]
for (const pattern of patterns) {
if (pattern.test(funcSource)) {
errors.push(
`Block "${block.type}": params function references raw subBlock id "${rawId}" which is deleted after canonical transformation. Use canonical param "${canonicalId}" instead.`
)
break
}
}
}
}
}
if (errors.length > 0) {
throw new Error(`Params function validation errors:\n${errors.join('\n')}`)
}
})
it('should have consistent required status across canonical param groups', () => {
const blocks = getAllBlocks()
const errors: string[] = []
for (const block of blocks) {
// Find all canonical groups (subBlocks with canonicalParamId)
const canonicalGroups = new Map<string, typeof block.subBlocks>()
for (const subBlock of block.subBlocks) {
if (subBlock.canonicalParamId) {
if (!canonicalGroups.has(subBlock.canonicalParamId)) {
canonicalGroups.set(subBlock.canonicalParamId, [])
}
canonicalGroups.get(subBlock.canonicalParamId)!.push(subBlock)
}
}
// For each canonical group, check that required status is consistent
for (const [canonicalId, subBlocks] of canonicalGroups) {
if (subBlocks.length < 2) continue // Single subblock, no consistency check needed
// Get required status for each subblock (handling both boolean and condition object)
const requiredStatuses = subBlocks.map((sb) => {
// If required is a condition object or function, we can't statically determine it
// so we skip those cases
if (typeof sb.required === 'object' || typeof sb.required === 'function') {
return 'dynamic'
}
return sb.required === true ? 'required' : 'optional'
})
// Filter out dynamic cases
const staticStatuses = requiredStatuses.filter((s) => s !== 'dynamic')
if (staticStatuses.length < 2) continue // Not enough static statuses to compare
// Check if all static statuses are the same
const hasRequired = staticStatuses.includes('required')
const hasOptional = staticStatuses.includes('optional')
if (hasRequired && hasOptional) {
const requiredSubBlocks = subBlocks
.filter((sb, i) => requiredStatuses[i] === 'required')
.map((sb) => `${sb.id} (${sb.mode || 'both'})`)
const optionalSubBlocks = subBlocks
.filter((sb, i) => requiredStatuses[i] === 'optional')
.map((sb) => `${sb.id} (${sb.mode || 'both'})`)
errors.push(
`Block "${block.type}": canonical param "${canonicalId}" has inconsistent required status. ` +
`Required: [${requiredSubBlocks.join(', ')}], Optional: [${optionalSubBlocks.join(', ')}]. ` +
`All subBlocks in a canonical group should have the same required status.`
)
}
}
}
if (errors.length > 0) {
throw new Error(`Required status consistency errors:\n${errors.join('\n')}`)
}
})
})
})

View File

@@ -216,8 +216,8 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
config: {
tool: (params) => params.operation as string,
params: (params) => {
const { fileUpload, fileReference, ...rest } = params
const normalizedFiles = normalizeFileInput(fileUpload || fileReference || params.files)
const { files, ...rest } = params
const normalizedFiles = normalizeFileInput(files)
return {
...rest,
...(normalizedFiles && { files: normalizedFiles }),
@@ -252,15 +252,7 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
},
files: {
type: 'array',
description: 'Files to include with the message',
},
fileUpload: {
type: 'array',
description: 'Uploaded files (basic mode)',
},
fileReference: {
type: 'json',
description: 'File reference from previous blocks (advanced mode)',
description: 'Files to include with the message (canonical param)',
},
historyLength: {
type: 'number',

View File

@@ -258,7 +258,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
const {
credential,
pageId,
manualPageId,
operation,
attachmentFile,
attachmentFileName,
@@ -266,7 +265,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
...rest
} = params
const effectivePageId = (pageId || manualPageId || '').trim()
const effectivePageId = pageId ? String(pageId).trim() : ''
const requiresPageId = [
'read',
@@ -314,8 +313,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
operation: { type: 'string', description: 'Operation to perform' },
domain: { type: 'string', description: 'Confluence domain' },
credential: { type: 'string', description: 'Confluence access token' },
pageId: { type: 'string', description: 'Page identifier' },
manualPageId: { type: 'string', description: 'Manual page identifier' },
pageId: { type: 'string', description: 'Page identifier (canonical param)' },
spaceId: { type: 'string', description: 'Space identifier' },
title: { type: 'string', description: 'Page title' },
content: { type: 'string', description: 'Page content' },
@@ -324,7 +322,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
comment: { type: 'string', description: 'Comment text' },
commentId: { type: 'string', description: 'Comment identifier' },
attachmentId: { type: 'string', description: 'Attachment identifier' },
attachmentFile: { type: 'json', description: 'File to upload as attachment' },
attachmentFile: { type: 'json', description: 'File to upload as attachment (canonical param)' },
attachmentFileName: { type: 'string', description: 'Custom file name for attachment' },
attachmentComment: { type: 'string', description: 'Comment for the attachment' },
labelName: { type: 'string', description: 'Label name' },
@@ -617,17 +615,14 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
const {
credential,
pageId,
manualPageId,
operation,
attachmentFileUpload,
attachmentFileReference,
attachmentFile,
attachmentFileName,
attachmentComment,
...rest
} = params
const effectivePageId = (pageId || manualPageId || '').trim()
const effectivePageId = pageId ? String(pageId).trim() : ''
const requiresPageId = [
'read',
@@ -651,8 +646,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
}
if (operation === 'upload_attachment') {
const fileInput = attachmentFileUpload || attachmentFileReference || attachmentFile
const normalizedFile = normalizeFileInput(fileInput, { single: true })
const normalizedFile = normalizeFileInput(attachmentFile, { single: true })
if (!normalizedFile) {
throw new Error('File is required for upload attachment operation.')
}
@@ -680,8 +674,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
operation: { type: 'string', description: 'Operation to perform' },
domain: { type: 'string', description: 'Confluence domain' },
credential: { type: 'string', description: 'Confluence access token' },
pageId: { type: 'string', description: 'Page identifier' },
manualPageId: { type: 'string', description: 'Manual page identifier' },
pageId: { type: 'string', description: 'Page identifier (canonical param)' },
spaceId: { type: 'string', description: 'Space identifier' },
title: { type: 'string', description: 'Page title' },
content: { type: 'string', description: 'Page content' },
@@ -690,9 +683,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
comment: { type: 'string', description: 'Comment text' },
commentId: { type: 'string', description: 'Comment identifier' },
attachmentId: { type: 'string', description: 'Attachment identifier' },
attachmentFile: { type: 'json', description: 'File to upload as attachment' },
attachmentFileUpload: { type: 'json', description: 'Uploaded file (basic mode)' },
attachmentFileReference: { type: 'json', description: 'File reference (advanced mode)' },
attachmentFile: { type: 'json', description: 'File to upload as attachment (canonical param)' },
attachmentFileName: { type: 'string', description: 'Custom file name for attachment' },
attachmentComment: { type: 'string', description: 'Comment for the attachment' },
labelName: { type: 'string', description: 'Label name' },

View File

@@ -584,7 +584,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
...commonParams,
channelId: params.channelId,
content: params.content,
files: normalizeFileInput(params.attachmentFiles || params.files),
files: normalizeFileInput(params.files),
}
}
case 'discord_get_messages':
@@ -773,8 +773,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
nick: { type: 'string', description: 'Member nickname' },
reason: { type: 'string', description: 'Reason for moderation action' },
archived: { type: 'string', description: 'Archive status (true/false)' },
attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' },
files: { type: 'array', description: 'Files to attach (UserFile array)' },
files: { type: 'array', description: 'Files to attach (canonical param)' },
limit: { type: 'number', description: 'Message limit' },
autoArchiveDuration: { type: 'number', description: 'Thread auto-archive duration in minutes' },
channelType: { type: 'number', description: 'Discord channel type (0=text, 2=voice, etc.)' },

View File

@@ -317,12 +317,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
params.maxResults = Number(params.maxResults)
}
// Normalize file input for upload operation
// Check all possible field IDs: uploadFile (basic), fileRef (advanced), fileContent (legacy)
const normalizedFile = normalizeFileInput(
params.uploadFile || params.fileRef || params.fileContent,
{ single: true }
)
// Normalize file input for upload operation - use canonical 'file' param
const normalizedFile = normalizeFileInput(params.file, { single: true })
if (normalizedFile) {
params.file = normalizedFile
}
@@ -361,10 +357,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
path: { type: 'string', description: 'Path in Dropbox' },
autorename: { type: 'boolean', description: 'Auto-rename on conflict' },
// Upload inputs
uploadFile: { type: 'json', description: 'Uploaded file (UserFile)' },
file: { type: 'json', description: 'File to upload (UserFile object)' },
fileRef: { type: 'json', description: 'File reference from previous block' },
fileContent: { type: 'string', description: 'Legacy: base64 encoded file content' },
file: { type: 'json', description: 'File to upload (canonical param)' },
fileName: { type: 'string', description: 'Optional filename' },
mode: { type: 'string', description: 'Write mode: add or overwrite' },
mute: { type: 'boolean', description: 'Mute notifications' },

View File

@@ -194,7 +194,8 @@ export const FileV2Block: BlockConfig<FileParserOutput> = {
fallbackToolId: 'file_parser_v2',
}),
params: (params) => {
const fileInput = params.file || params.filePath || params.fileInput
// Use canonical 'fileInput' param directly
const fileInput = params.fileInput
if (!fileInput) {
logger.error('No file input provided')
throw new Error('File is required')
@@ -228,9 +229,7 @@ export const FileV2Block: BlockConfig<FileParserOutput> = {
},
},
inputs: {
fileInput: { type: 'json', description: 'File input (upload or URL reference)' },
filePath: { type: 'string', description: 'File URL (advanced mode)' },
file: { type: 'json', description: 'Uploaded file data (basic mode)' },
fileInput: { type: 'json', description: 'File input (canonical param)' },
fileType: { type: 'string', description: 'File type' },
},
outputs: {
@@ -283,7 +282,8 @@ export const FileV3Block: BlockConfig<FileParserV3Output> = {
config: {
tool: () => 'file_parser_v3',
params: (params) => {
const fileInput = params.fileInput ?? params.file ?? params.fileUrl ?? params.filePath
// Use canonical 'fileInput' param directly
const fileInput = params.fileInput
if (!fileInput) {
logger.error('No file input provided')
throw new Error('File input is required')
@@ -321,9 +321,7 @@ export const FileV3Block: BlockConfig<FileParserV3Output> = {
},
},
inputs: {
fileInput: { type: 'json', description: 'File input (upload or URL)' },
fileUrl: { type: 'string', description: 'External file URL (advanced mode)' },
file: { type: 'json', description: 'Uploaded file data (basic mode)' },
fileInput: { type: 'json', description: 'File input (canonical param)' },
fileType: { type: 'string', description: 'File type' },
},
outputs: {

View File

@@ -461,12 +461,11 @@ Return ONLY the summary text - no quotes, no labels.`,
return baseParams
case 'fireflies_upload_audio': {
// Support both file upload and URL
// Support both file upload and URL - use canonical 'audioFile' param
const audioUrl = params.audioUrl?.trim()
const audioFile = params.audioFile
const audioFileReference = params.audioFileReference
if (!audioUrl && !audioFile && !audioFileReference) {
if (!audioUrl && !audioFile) {
throw new Error('Either audio file or audio URL is required.')
}
@@ -474,7 +473,6 @@ Return ONLY the summary text - no quotes, no labels.`,
...baseParams,
audioUrl: audioUrl || undefined,
audioFile: audioFile || undefined,
audioFileReference: audioFileReference || undefined,
title: params.title?.trim() || undefined,
language: params.language?.trim() || undefined,
attendees: params.attendees?.trim() || undefined,
@@ -548,8 +546,7 @@ Return ONLY the summary text - no quotes, no labels.`,
hostEmail: { type: 'string', description: 'Filter by host email' },
participants: { type: 'string', description: 'Filter by participants (comma-separated)' },
limit: { type: 'number', description: 'Maximum results to return' },
audioFile: { type: 'json', description: 'Audio/video file (UserFile)' },
audioFileReference: { type: 'json', description: 'Audio/video file reference' },
audioFile: { type: 'json', description: 'Audio/video file (canonical param)' },
audioUrl: { type: 'string', description: 'Public URL to audio file' },
title: { type: 'string', description: 'Meeting title' },
language: { type: 'string', description: 'Language code for transcription' },
@@ -620,9 +617,8 @@ export const FirefliesV2Block: BlockConfig<FirefliesResponse> = {
}
if (params.operation === 'fireflies_upload_audio') {
const audioFile = normalizeFileInput(params.audioFile || params.audioFileReference, {
single: true,
})
// Use canonical 'audioFile' param directly
const audioFile = normalizeFileInput(params.audioFile, { single: true })
if (!audioFile) {
throw new Error('Audio file is required.')
}
@@ -635,7 +631,6 @@ export const FirefliesV2Block: BlockConfig<FirefliesResponse> = {
...params,
audioUrl,
audioFile: undefined,
audioFileReference: undefined,
})
}
@@ -643,8 +638,5 @@ export const FirefliesV2Block: BlockConfig<FirefliesResponse> = {
},
},
},
inputs: {
...firefliesV2Inputs,
audioFileReference: { type: 'json', description: 'Audio/video file reference' },
},
inputs: firefliesV2Inputs,
}

View File

@@ -362,10 +362,10 @@ Return ONLY the search query - no explanations, no extra text.`,
},
// Add/Remove Label - Label selector (basic mode)
{
id: 'labelManagement',
id: 'labelSelector',
title: 'Label',
type: 'folder-selector',
canonicalParamId: 'labelIds',
canonicalParamId: 'manageLabelId',
serviceId: 'gmail',
requiredScopes: ['https://www.googleapis.com/auth/gmail.labels'],
placeholder: 'Select label',
@@ -376,10 +376,10 @@ Return ONLY the search query - no explanations, no extra text.`,
},
// Add/Remove Label - Manual label input (advanced mode)
{
id: 'manualLabelManagement',
id: 'manualLabelId',
title: 'Label',
type: 'short-input',
canonicalParamId: 'labelIds',
canonicalParamId: 'manageLabelId',
placeholder: 'Enter label ID (e.g., INBOX, Label_123)',
mode: 'advanced',
condition: { field: 'operation', value: ['add_label_gmail', 'remove_label_gmail'] },
@@ -408,38 +408,33 @@ Return ONLY the search query - no explanations, no extra text.`,
const {
credential,
folder,
manualFolder,
destinationLabel,
manualDestinationLabel,
sourceLabel,
manualSourceLabel,
addLabelIds,
removeLabelIds,
moveMessageId,
actionMessageId,
labelActionMessageId,
labelManagement,
manualLabelManagement,
attachmentFiles,
manageLabelId,
attachments,
...rest
} = params
// Handle both selector and manual folder input
const effectiveFolder = (folder || manualFolder || '').trim()
// Use canonical 'folder' param directly
const effectiveFolder = folder ? String(folder).trim() : ''
if (rest.operation === 'read_gmail') {
rest.folder = effectiveFolder || 'INBOX'
}
// Handle move operation
// Handle move operation - use canonical params addLabelIds and removeLabelIds
if (rest.operation === 'move_gmail') {
if (moveMessageId) {
rest.messageId = moveMessageId
}
if (!rest.addLabelIds) {
rest.addLabelIds = (destinationLabel || manualDestinationLabel || '').trim()
if (addLabelIds) {
rest.addLabelIds = String(addLabelIds).trim()
}
if (!rest.removeLabelIds) {
rest.removeLabelIds = (sourceLabel || manualSourceLabel || '').trim()
if (removeLabelIds) {
rest.removeLabelIds = String(removeLabelIds).trim()
}
}
@@ -462,13 +457,13 @@ Return ONLY the search query - no explanations, no extra text.`,
if (labelActionMessageId) {
rest.messageId = labelActionMessageId
}
if (!rest.labelIds) {
rest.labelIds = (labelManagement || manualLabelManagement || '').trim()
if (manageLabelId) {
rest.labelIds = String(manageLabelId).trim()
}
}
// Normalize attachments for send/draft operations
const normalizedAttachments = normalizeFileInput(attachmentFiles || attachments)
// Normalize attachments for send/draft operations - use canonical 'attachments' param
const normalizedAttachments = normalizeFileInput(attachments)
return {
...rest,
@@ -493,10 +488,9 @@ Return ONLY the search query - no explanations, no extra text.`,
},
cc: { type: 'string', description: 'CC recipients (comma-separated)' },
bcc: { type: 'string', description: 'BCC recipients (comma-separated)' },
attachments: { type: 'array', description: 'Files to attach (UserFile array)' },
attachments: { type: 'array', description: 'Files to attach (canonical param)' },
// Read operation inputs
folder: { type: 'string', description: 'Gmail folder' },
manualFolder: { type: 'string', description: 'Manual folder name' },
folder: { type: 'string', description: 'Gmail folder (canonical param)' },
readMessageId: { type: 'string', description: 'Message identifier for reading specific email' },
unreadOnly: { type: 'boolean', description: 'Unread messages only' },
includeAttachments: { type: 'boolean', description: 'Include email attachments' },
@@ -505,18 +499,16 @@ Return ONLY the search query - no explanations, no extra text.`,
maxResults: { type: 'number', description: 'Maximum results' },
// Move operation inputs
moveMessageId: { type: 'string', description: 'Message ID to move' },
destinationLabel: { type: 'string', description: 'Destination label ID' },
manualDestinationLabel: { type: 'string', description: 'Manual destination label ID' },
sourceLabel: { type: 'string', description: 'Source label ID to remove' },
manualSourceLabel: { type: 'string', description: 'Manual source label ID' },
addLabelIds: { type: 'string', description: 'Label IDs to add' },
removeLabelIds: { type: 'string', description: 'Label IDs to remove' },
addLabelIds: { type: 'string', description: 'Label IDs to add (canonical param)' },
removeLabelIds: { type: 'string', description: 'Label IDs to remove (canonical param)' },
// Action operation inputs
actionMessageId: { type: 'string', description: 'Message ID for actions' },
labelActionMessageId: { type: 'string', description: 'Message ID for label actions' },
labelManagement: { type: 'string', description: 'Label ID for management' },
manualLabelManagement: { type: 'string', description: 'Manual label ID' },
labelIds: { type: 'string', description: 'Label IDs for add/remove operations' },
manageLabelId: {
type: 'string',
description: 'Label ID for add/remove operations (canonical param)',
},
labelIds: { type: 'string', description: 'Label IDs to monitor (trigger)' },
},
outputs: {
// Tool outputs

View File

@@ -517,21 +517,17 @@ Return ONLY the natural language event text - no explanations.`,
attendees,
replaceExisting,
calendarId,
manualCalendarId,
destinationCalendar,
manualDestinationCalendarId,
destinationCalendarId,
...rest
} = params
// Handle calendar ID (selector or manual)
const effectiveCalendarId = (calendarId || manualCalendarId || '').trim()
// Use canonical 'calendarId' param directly
const effectiveCalendarId = calendarId ? String(calendarId).trim() : ''
// Handle destination calendar ID for move operation (selector or manual)
const effectiveDestinationCalendarId = (
destinationCalendar ||
manualDestinationCalendarId ||
''
).trim()
// Use canonical 'destinationCalendarId' param directly
const effectiveDestinationCalendarId = destinationCalendarId
? String(destinationCalendarId).trim()
: ''
const processedParams: Record<string, any> = {
...rest,
@@ -589,8 +585,7 @@ Return ONLY the natural language event text - no explanations.`,
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Google Calendar access token' },
calendarId: { type: 'string', description: 'Calendar identifier' },
manualCalendarId: { type: 'string', description: 'Manual calendar identifier' },
calendarId: { type: 'string', description: 'Calendar identifier (canonical param)' },
// Create/Update operation inputs
summary: { type: 'string', description: 'Event title' },
@@ -609,8 +604,10 @@ Return ONLY the natural language event text - no explanations.`,
eventId: { type: 'string', description: 'Event identifier' },
// Move operation inputs
destinationCalendar: { type: 'string', description: 'Destination calendar selector' },
manualDestinationCalendarId: { type: 'string', description: 'Manual destination calendar ID' },
destinationCalendarId: {
type: 'string',
description: 'Destination calendar ID (canonical param)',
},
// List Calendars operation inputs
minAccessRole: { type: 'string', description: 'Minimum access role filter' },

View File

@@ -157,11 +157,10 @@ Return ONLY the document content - no explanations, no extra text.`,
}
},
params: (params) => {
const { credential, documentId, manualDocumentId, folderSelector, folderId, ...rest } =
params
const { credential, documentId, folderId, ...rest } = params
const effectiveDocumentId = (documentId || manualDocumentId || '').trim()
const effectiveFolderId = (folderSelector || folderId || '').trim()
const effectiveDocumentId = documentId ? String(documentId).trim() : ''
const effectiveFolderId = folderId ? String(folderId).trim() : ''
return {
...rest,
@@ -175,11 +174,9 @@ Return ONLY the document content - no explanations, no extra text.`,
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Google Docs access token' },
documentId: { type: 'string', description: 'Document identifier' },
manualDocumentId: { type: 'string', description: 'Manual document identifier' },
documentId: { type: 'string', description: 'Document identifier (canonical param)' },
title: { type: 'string', description: 'Document title' },
folderSelector: { type: 'string', description: 'Selected folder' },
folderId: { type: 'string', description: 'Folder identifier' },
folderId: { type: 'string', description: 'Parent folder identifier (canonical param)' },
content: { type: 'string', description: 'Document content' },
},
outputs: {

View File

@@ -804,7 +804,6 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
listPermissionsFileId,
// File upload
file,
fileUpload,
mimeType,
shareType,
starred,
@@ -813,7 +812,7 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
} = params
// Normalize file input - handles both basic (file-upload) and advanced (short-input) modes
const normalizedFile = normalizeFileInput(file ?? fileUpload, { single: true })
const normalizedFile = normalizeFileInput(file, { single: true })
// Resolve folderId based on operation
let effectiveFolderId: string | undefined

View File

@@ -47,10 +47,11 @@ export const GoogleFormsBlock: BlockConfig = {
},
// Form selector (basic mode)
{
id: 'formId',
id: 'formSelector',
title: 'Select Form',
type: 'file-selector',
canonicalParamId: 'formId',
required: true,
serviceId: 'google-forms',
requiredScopes: [],
mimeType: 'application/vnd.google-apps.form',
@@ -234,8 +235,7 @@ Example for "Add a required multiple choice question about favorite color":
const {
credential,
operation,
formId,
manualFormId,
formId, // Canonical param from formSelector (basic) or manualFormId (advanced)
responseId,
pageSize,
title,
@@ -252,7 +252,7 @@ Example for "Add a required multiple choice question about favorite color":
} = params
const baseParams = { ...rest, credential }
const effectiveFormId = (formId || manualFormId || '').toString().trim() || undefined
const effectiveFormId = formId ? String(formId).trim() : undefined
switch (operation) {
case 'get_responses':
@@ -321,8 +321,7 @@ Example for "Add a required multiple choice question about favorite color":
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Google OAuth credential' },
formId: { type: 'string', description: 'Google Form ID (from selector)' },
manualFormId: { type: 'string', description: 'Google Form ID (manual entry)' },
formId: { type: 'string', description: 'Google Form ID' },
responseId: { type: 'string', description: 'Specific response ID' },
pageSize: { type: 'string', description: 'Max responses to retrieve' },
title: { type: 'string', description: 'Form title for creation' },

View File

@@ -246,11 +246,11 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
}
},
params: (params) => {
const { credential, values, spreadsheetId, manualSpreadsheetId, ...rest } = params
const { credential, values, spreadsheetId, ...rest } = params
const parsedValues = values ? JSON.parse(values as string) : undefined
const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim()
const effectiveSpreadsheetId = spreadsheetId ? String(spreadsheetId).trim() : ''
if (!effectiveSpreadsheetId) {
throw new Error('Spreadsheet ID is required.')
@@ -268,8 +268,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Google Sheets access token' },
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' },
manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' },
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier (canonical param)' },
range: { type: 'string', description: 'Cell range' },
values: { type: 'string', description: 'Cell values data' },
valueInputOption: { type: 'string', description: 'Value input option' },
@@ -719,9 +718,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
credential,
values,
spreadsheetId,
manualSpreadsheetId,
sheetName,
manualSheetName,
cellRange,
title,
sheetTitles,
@@ -746,9 +743,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
}
}
const effectiveSpreadsheetId = (
(spreadsheetId || manualSpreadsheetId || '') as string
).trim()
const effectiveSpreadsheetId = spreadsheetId ? String(spreadsheetId).trim() : ''
if (!effectiveSpreadsheetId) {
throw new Error('Spreadsheet ID is required.')
@@ -804,7 +799,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
}
// Handle read/write/update/append/clear operations (require sheet name)
const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim()
const effectiveSheetName = sheetName ? String(sheetName).trim() : ''
if (!effectiveSheetName) {
throw new Error('Sheet name is required. Please select or enter a sheet name.')
@@ -826,10 +821,8 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Google Sheets access token' },
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' },
manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' },
sheetName: { type: 'string', description: 'Name of the sheet/tab' },
manualSheetName: { type: 'string', description: 'Manual sheet name entry' },
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier (canonical param)' },
sheetName: { type: 'string', description: 'Name of the sheet/tab (canonical param)' },
cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' },
values: { type: 'string', description: 'Cell values data' },
valueInputOption: { type: 'string', description: 'Value input option' },

View File

@@ -664,8 +664,6 @@ Return ONLY the text content - no explanations, no markdown formatting markers,
const {
credential,
presentationId,
manualPresentationId,
folderSelector,
folderId,
slideIndex,
createContent,
@@ -675,8 +673,8 @@ Return ONLY the text content - no explanations, no markdown formatting markers,
...rest
} = params
const effectivePresentationId = (presentationId || manualPresentationId || '').trim()
const effectiveFolderId = (folderSelector || folderId || '').trim()
const effectivePresentationId = presentationId ? String(presentationId).trim() : ''
const effectiveFolderId = folderId ? String(folderId).trim() : ''
const result: Record<string, any> = {
...rest,
@@ -802,15 +800,13 @@ Return ONLY the text content - no explanations, no markdown formatting markers,
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Google Slides access token' },
presentationId: { type: 'string', description: 'Presentation identifier' },
manualPresentationId: { type: 'string', description: 'Manual presentation identifier' },
presentationId: { type: 'string', description: 'Presentation identifier (canonical param)' },
// Write operation
slideIndex: { type: 'number', description: 'Slide index to write to' },
content: { type: 'string', description: 'Slide content' },
// Create operation
title: { type: 'string', description: 'Presentation title' },
folderSelector: { type: 'string', description: 'Selected folder' },
folderId: { type: 'string', description: 'Folder identifier' },
folderId: { type: 'string', description: 'Parent folder identifier (canonical param)' },
createContent: { type: 'string', description: 'Initial slide content' },
// Replace all text operation
findText: { type: 'string', description: 'Text to find' },
@@ -826,8 +822,6 @@ Return ONLY the text content - no explanations, no markdown formatting markers,
placeholderIdMappings: { type: 'string', description: 'JSON array of placeholder ID mappings' },
// Add image operation
pageObjectId: { type: 'string', description: 'Slide object ID for image' },
imageFile: { type: 'json', description: 'Uploaded image (UserFile)' },
imageUrl: { type: 'string', description: 'Image URL or reference' },
imageSource: { type: 'json', description: 'Image source (file or URL)' },
imageWidth: { type: 'number', description: 'Image width in points' },
imageHeight: { type: 'number', description: 'Image height in points' },
@@ -936,11 +930,12 @@ const googleSlidesV2SubBlocks = (GoogleSlidesBlock.subBlocks || []).flatMap((sub
})
const googleSlidesV2Inputs = GoogleSlidesBlock.inputs
? Object.fromEntries(
Object.entries(GoogleSlidesBlock.inputs).filter(
([key]) => key !== 'imageUrl' && key !== 'imageSource'
)
)
? {
...Object.fromEntries(
Object.entries(GoogleSlidesBlock.inputs).filter(([key]) => key !== 'imageSource')
),
imageFile: { type: 'json', description: 'Image source (file or URL)' },
}
: {}
export const GoogleSlidesV2Block: BlockConfig<GoogleSlidesResponse> = {
@@ -961,8 +956,7 @@ export const GoogleSlidesV2Block: BlockConfig<GoogleSlidesResponse> = {
}
if (params.operation === 'add_image') {
const imageInput = params.imageFile || params.imageFileReference || params.imageSource
const fileObject = normalizeFileInput(imageInput, { single: true })
const fileObject = normalizeFileInput(params.imageFile, { single: true })
if (!fileObject) {
throw new Error('Image file is required.')
}
@@ -974,8 +968,6 @@ export const GoogleSlidesV2Block: BlockConfig<GoogleSlidesResponse> = {
return baseParams({
...params,
imageUrl,
imageFileReference: undefined,
imageSource: undefined,
})
}
@@ -983,8 +975,5 @@ export const GoogleSlidesV2Block: BlockConfig<GoogleSlidesResponse> = {
},
},
},
inputs: {
...googleSlidesV2Inputs,
imageFileReference: { type: 'json', description: 'Image file reference' },
},
inputs: googleSlidesV2Inputs,
}

View File

@@ -615,8 +615,9 @@ Return ONLY the comment text - no explanations.`,
],
config: {
tool: (params) => {
const effectiveProjectId = (params.projectId || params.manualProjectId || '').trim()
const effectiveIssueKey = (params.issueKey || params.manualIssueKey || '').trim()
// Use canonical param IDs (raw subBlock IDs are deleted after serialization)
const effectiveProjectId = params.projectId ? String(params.projectId).trim() : ''
const effectiveIssueKey = params.issueKey ? String(params.issueKey).trim() : ''
switch (params.operation) {
case 'read':
@@ -676,11 +677,11 @@ Return ONLY the comment text - no explanations.`,
}
},
params: (params) => {
const { credential, projectId, manualProjectId, issueKey, manualIssueKey, ...rest } = params
const { credential, projectId, issueKey, ...rest } = params
// Use the selected IDs or the manually entered ones
const effectiveProjectId = (projectId || manualProjectId || '').trim()
const effectiveIssueKey = (issueKey || manualIssueKey || '').trim()
// Use canonical param IDs (raw subBlock IDs are deleted after serialization)
const effectiveProjectId = projectId ? String(projectId).trim() : ''
const effectiveIssueKey = issueKey ? String(issueKey).trim() : ''
const baseParams = {
credential,
@@ -748,27 +749,20 @@ Return ONLY the comment text - no explanations.`,
}
}
case 'read': {
// Check for project ID from either source
const projectForRead = (params.projectId || params.manualProjectId || '').trim()
const issueForRead = (params.issueKey || params.manualIssueKey || '').trim()
if (!issueForRead) {
if (!effectiveIssueKey) {
throw new Error(
'Select a project to read issues, or provide an issue key to read a single issue.'
)
}
return {
...baseParams,
issueKey: issueForRead,
issueKey: effectiveIssueKey,
// Include projectId if available for context
...(projectForRead && { projectId: projectForRead }),
...(effectiveProjectId && { projectId: effectiveProjectId }),
}
}
case 'read-bulk': {
// Check both projectId and manualProjectId directly from params
const finalProjectId = params.projectId || params.manualProjectId || ''
if (!finalProjectId) {
if (!effectiveProjectId) {
throw new Error(
'Project ID is required. Please select a project or enter a project ID manually.'
)
@@ -870,7 +864,7 @@ Return ONLY the comment text - no explanations.`,
if (!effectiveIssueKey) {
throw new Error('Issue Key is required to add attachments.')
}
const normalizedFiles = normalizeFileInput(params.attachmentFiles || params.files)
const normalizedFiles = normalizeFileInput(params.files)
if (!normalizedFiles || normalizedFiles.length === 0) {
throw new Error('At least one attachment file is required.')
}
@@ -990,10 +984,8 @@ Return ONLY the comment text - no explanations.`,
operation: { type: 'string', description: 'Operation to perform' },
domain: { type: 'string', description: 'Jira domain' },
credential: { type: 'string', description: 'Jira access token' },
issueKey: { type: 'string', description: 'Issue key identifier' },
projectId: { type: 'string', description: 'Project identifier' },
manualProjectId: { type: 'string', description: 'Manual project identifier' },
manualIssueKey: { type: 'string', description: 'Manual issue key' },
issueKey: { type: 'string', description: 'Issue key identifier (canonical param)' },
projectId: { type: 'string', description: 'Project identifier (canonical param)' },
// Update/Write operation inputs
summary: { type: 'string', description: 'Issue summary' },
description: { type: 'string', description: 'Issue description' },
@@ -1024,8 +1016,7 @@ 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)' },
files: { type: 'array', description: 'Files to attach (canonical param)' },
attachmentId: { type: 'string', description: 'Attachment ID for delete operation' },
// Worklog operation inputs
timeSpentSeconds: {

View File

@@ -1476,9 +1476,9 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
return params.operation || 'linear_read_issues'
},
params: (params) => {
// Handle both selector and manual inputs
const effectiveTeamId = (params.teamId || params.manualTeamId || '').trim()
const effectiveProjectId = (params.projectId || params.manualProjectId || '').trim()
// Use canonical param IDs (raw subBlock IDs are deleted after serialization)
const effectiveTeamId = params.teamId ? String(params.teamId).trim() : ''
const effectiveProjectId = params.projectId ? String(params.projectId).trim() : ''
// Base params that most operations need
const baseParams: Record<string, any> = {
@@ -1774,16 +1774,11 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
if (!params.issueId?.trim()) {
throw new Error('Issue ID is required.')
}
// Normalize file inputs - handles JSON stringified values from advanced mode
const attachmentFile =
normalizeFileInput(params.attachmentFileUpload, {
single: true,
errorMessage: 'Attachment file must be a single file.',
}) ||
normalizeFileInput(params.file, {
single: true,
errorMessage: 'Attachment file must be a single file.',
})
// Normalize file input - use canonical param 'file' (raw subBlock IDs are deleted after serialization)
const attachmentFile = normalizeFileInput(params.file, {
single: true,
errorMessage: 'Attachment file must be a single file.',
})
const attachmentUrl =
params.url?.trim() ||
(attachmentFile ? (attachmentFile as { url?: string }).url : undefined)
@@ -2261,10 +2256,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Linear access token' },
teamId: { type: 'string', description: 'Linear team identifier' },
projectId: { type: 'string', description: 'Linear project identifier' },
manualTeamId: { type: 'string', description: 'Manual team identifier' },
manualProjectId: { type: 'string', description: 'Manual project identifier' },
teamId: { type: 'string', description: 'Linear team identifier (canonical param)' },
projectId: { type: 'string', description: 'Linear project identifier (canonical param)' },
issueId: { type: 'string', description: 'Issue identifier' },
title: { type: 'string', description: 'Title' },
description: { type: 'string', description: 'Description' },
@@ -2294,8 +2287,7 @@ 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)' },
file: { type: 'json', description: 'File to attach (canonical param)' },
attachmentTitle: { type: 'string', description: 'Attachment title' },
attachmentId: { type: 'string', description: 'Attachment identifier' },
relationType: { type: 'string', description: 'Relation type' },

View File

@@ -241,17 +241,10 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
}
},
params: (params) => {
const {
credential,
values,
spreadsheetId,
manualSpreadsheetId,
tableName,
worksheetName,
...rest
} = params
const { credential, values, spreadsheetId, tableName, worksheetName, ...rest } = params
const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim()
// Use canonical param ID (raw subBlock IDs are deleted after serialization)
const effectiveSpreadsheetId = spreadsheetId ? String(spreadsheetId).trim() : ''
let parsedValues
try {
@@ -300,8 +293,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Microsoft Excel access token' },
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' },
manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' },
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier (canonical param)' },
range: { type: 'string', description: 'Cell range' },
tableName: { type: 'string', description: 'Table name' },
worksheetName: { type: 'string', description: 'Worksheet name' },
@@ -505,21 +497,13 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
fallbackToolId: 'microsoft_excel_read_v2',
}),
params: (params) => {
const {
credential,
values,
spreadsheetId,
manualSpreadsheetId,
sheetName,
manualSheetName,
cellRange,
...rest
} = params
const { credential, values, spreadsheetId, sheetName, cellRange, ...rest } = params
const parsedValues = values ? JSON.parse(values as string) : undefined
const effectiveSpreadsheetId = (spreadsheetId || manualSpreadsheetId || '').trim()
const effectiveSheetName = ((sheetName || manualSheetName || '') as string).trim()
// Use canonical param IDs (raw subBlock IDs are deleted after serialization)
const effectiveSpreadsheetId = spreadsheetId ? String(spreadsheetId).trim() : ''
const effectiveSheetName = sheetName ? String(sheetName).trim() : ''
if (!effectiveSpreadsheetId) {
throw new Error('Spreadsheet ID is required.')
@@ -543,10 +527,8 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Microsoft Excel access token' },
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier' },
manualSpreadsheetId: { type: 'string', description: 'Manual spreadsheet identifier' },
sheetName: { type: 'string', description: 'Name of the sheet/tab' },
manualSheetName: { type: 'string', description: 'Manual sheet name entry' },
spreadsheetId: { type: 'string', description: 'Spreadsheet identifier (canonical param)' },
sheetName: { type: 'string', description: 'Name of the sheet/tab (canonical param)' },
cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' },
values: { type: 'string', description: 'Cell values data' },
valueInputOption: { type: 'string', description: 'Value input option' },

View File

@@ -87,9 +87,9 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
dependsOn: ['credential'],
},
// Task ID selector - for read_task
// Task ID selector - for read_task (basic mode)
{
id: 'taskId',
id: 'taskSelector',
title: 'Task ID',
type: 'file-selector',
placeholder: 'Select a task',
@@ -97,24 +97,24 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
condition: { field: 'operation', value: ['read_task'] },
dependsOn: ['credential', 'planId'],
mode: 'basic',
canonicalParamId: 'taskId',
canonicalParamId: 'readTaskId',
},
// Manual Task ID - for read_task advanced mode
// Manual Task ID - for read_task (advanced mode)
{
id: 'manualTaskId',
id: 'manualReadTaskId',
title: 'Manual Task ID',
type: 'short-input',
placeholder: 'Enter the task ID',
condition: { field: 'operation', value: ['read_task'] },
dependsOn: ['credential', 'planId'],
mode: 'advanced',
canonicalParamId: 'taskId',
canonicalParamId: 'readTaskId',
},
// Task ID for update/delete operations
// Task ID for update/delete operations (no basic/advanced split, just one input)
{
id: 'taskIdForUpdate',
id: 'updateTaskId',
title: 'Task ID',
type: 'short-input',
placeholder: 'Enter the task ID',
@@ -123,7 +123,6 @@ export const MicrosoftPlannerBlock: BlockConfig<MicrosoftPlannerResponse> = {
value: ['update_task', 'delete_task', 'get_task_details', 'update_task_details'],
},
dependsOn: ['credential'],
canonicalParamId: 'taskId',
},
// Bucket ID for bucket operations
@@ -347,9 +346,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
operation,
groupId,
planId,
taskId,
manualTaskId,
taskIdForUpdate,
readTaskId, // Canonical param from taskSelector (basic) or manualReadTaskId (advanced) for read_task
updateTaskId, // Task ID for update/delete operations
bucketId,
bucketIdForRead,
title,
@@ -372,8 +370,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
credential,
}
// Handle different task ID fields
const effectiveTaskId = (taskIdForUpdate || taskId || manualTaskId || '').trim()
// Handle different task ID fields based on operation
const effectiveReadTaskId = readTaskId ? String(readTaskId).trim() : ''
const effectiveUpdateTaskId = updateTaskId ? String(updateTaskId).trim() : ''
const effectiveBucketId = (bucketIdForRead || bucketId || '').trim()
// List Plans
@@ -466,10 +465,9 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
// Read Task
if (operation === 'read_task') {
const readParams: MicrosoftPlannerBlockParams = { ...baseParams }
const readTaskId = (taskId || manualTaskId || '').trim()
if (readTaskId) {
readParams.taskId = readTaskId
if (effectiveReadTaskId) {
readParams.taskId = effectiveReadTaskId
} else if (planId?.trim()) {
readParams.planId = planId.trim()
}
@@ -510,7 +508,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
// Update Task
if (operation === 'update_task') {
if (!effectiveTaskId) {
if (!effectiveUpdateTaskId) {
throw new Error('Task ID is required to update a task.')
}
if (!etag?.trim()) {
@@ -519,7 +517,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
const updateParams: MicrosoftPlannerBlockParams = {
...baseParams,
taskId: effectiveTaskId,
taskId: effectiveUpdateTaskId,
etag: etag.trim(),
}
@@ -550,7 +548,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
// Delete Task
if (operation === 'delete_task') {
if (!effectiveTaskId) {
if (!effectiveUpdateTaskId) {
throw new Error('Task ID is required to delete a task.')
}
if (!etag?.trim()) {
@@ -558,25 +556,25 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
}
return {
...baseParams,
taskId: effectiveTaskId,
taskId: effectiveUpdateTaskId,
etag: etag.trim(),
}
}
// Get Task Details
if (operation === 'get_task_details') {
if (!effectiveTaskId) {
if (!effectiveUpdateTaskId) {
throw new Error('Task ID is required to get task details.')
}
return {
...baseParams,
taskId: effectiveTaskId,
taskId: effectiveUpdateTaskId,
}
}
// Update Task Details
if (operation === 'update_task_details') {
if (!effectiveTaskId) {
if (!effectiveUpdateTaskId) {
throw new Error('Task ID is required to update task details.')
}
if (!etag?.trim()) {
@@ -585,7 +583,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
const updateDetailsParams: MicrosoftPlannerBlockParams = {
...baseParams,
taskId: effectiveTaskId,
taskId: effectiveUpdateTaskId,
etag: etag.trim(),
}
@@ -614,9 +612,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
credential: { type: 'string', description: 'Microsoft account credential' },
groupId: { type: 'string', description: 'Microsoft 365 group ID' },
planId: { type: 'string', description: 'Plan ID' },
taskId: { type: 'string', description: 'Task ID' },
manualTaskId: { type: 'string', description: 'Manual Task ID' },
taskIdForUpdate: { type: 'string', description: 'Task ID for update operations' },
readTaskId: { type: 'string', description: 'Task ID for read operation' },
updateTaskId: { type: 'string', description: 'Task ID for update/delete operations' },
bucketId: { type: 'string', description: 'Bucket ID' },
bucketIdForRead: { type: 'string', description: 'Bucket ID for read operations' },
title: { type: 'string', description: 'Task title' },

View File

@@ -71,7 +71,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
required: true,
},
{
id: 'teamId',
id: 'teamSelector',
title: 'Select Team',
type: 'file-selector',
canonicalParamId: 'teamId',
@@ -114,7 +114,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
},
},
{
id: 'chatId',
id: 'chatSelector',
title: 'Select Chat',
type: 'file-selector',
canonicalParamId: 'chatId',
@@ -141,14 +141,14 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
},
},
{
id: 'channelId',
id: 'channelSelector',
title: 'Select Channel',
type: 'file-selector',
canonicalParamId: 'channelId',
serviceId: 'microsoft-teams',
requiredScopes: [],
placeholder: 'Select a channel',
dependsOn: ['credential', 'teamId'],
dependsOn: ['credential', 'teamSelector'],
mode: 'basic',
condition: {
field: 'operation',
@@ -249,7 +249,7 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
},
// Variable reference (advanced mode)
{
id: 'files',
id: 'fileReferences',
title: 'File Attachments',
type: 'short-input',
canonicalParamId: 'files',
@@ -317,23 +317,19 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
const {
credential,
operation,
teamId,
manualTeamId,
chatId,
manualChatId,
channelId,
manualChannelId,
attachmentFiles,
files,
teamId, // Canonical param from teamSelector (basic) or manualTeamId (advanced)
chatId, // Canonical param from chatSelector (basic) or manualChatId (advanced)
channelId, // Canonical param from channelSelector (basic) or manualChannelId (advanced)
files, // Canonical param from attachmentFiles (basic) or fileReferences (advanced)
messageId,
reactionType,
includeAttachments,
...rest
} = params
const effectiveTeamId = (teamId || manualTeamId || '').trim()
const effectiveChatId = (chatId || manualChatId || '').trim()
const effectiveChannelId = (channelId || manualChannelId || '').trim()
const effectiveTeamId = teamId ? String(teamId).trim() : ''
const effectiveChatId = chatId ? String(chatId).trim() : ''
const effectiveChannelId = channelId ? String(channelId).trim() : ''
const baseParams: Record<string, any> = {
...rest,
@@ -344,9 +340,9 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
baseParams.includeAttachments = true
}
// Add files if provided
// Add files if provided (canonical param from attachmentFiles or fileReferences)
if (operation === 'write_chat' || operation === 'write_channel') {
const normalizedFiles = normalizeFileInput(attachmentFiles || files)
const normalizedFiles = normalizeFileInput(files)
if (normalizedFiles) {
baseParams.files = normalizedFiles
}
@@ -440,12 +436,11 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
type: 'string',
description: 'Message identifier for update/delete/reply/reaction operations',
},
chatId: { type: 'string', description: 'Chat identifier' },
manualChatId: { type: 'string', description: 'Manual chat identifier' },
channelId: { type: 'string', description: 'Channel identifier' },
manualChannelId: { type: 'string', description: 'Manual channel identifier' },
// Canonical params (used by params function)
teamId: { type: 'string', description: 'Team identifier' },
manualTeamId: { type: 'string', description: 'Manual team identifier' },
chatId: { type: 'string', description: 'Chat identifier' },
channelId: { type: 'string', description: 'Channel identifier' },
files: { type: 'array', description: 'Files to attach' },
content: {
type: 'string',
description: 'Message content. Mention users with <at>userName</at>',
@@ -455,8 +450,6 @@ export const MicrosoftTeamsBlock: BlockConfig<MicrosoftTeamsResponse> = {
type: 'boolean',
description: 'Download and include message attachments',
},
attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' },
files: { type: 'array', description: 'Files to attach (UserFile array)' },
},
outputs: {
content: { type: 'string', description: 'Formatted message content from chat/channel' },

View File

@@ -215,8 +215,8 @@ export const MistralParseV2Block: BlockConfig<MistralParserOutput> = {
resultType: params.resultType || 'markdown',
}
// Original V2 pattern: fileUpload (basic) or filePath (advanced) or document (wired)
const documentInput = params.fileUpload || params.filePath || params.document
// Use canonical document param directly
const documentInput = params.document
if (!documentInput) {
throw new Error('PDF document is required')
}
@@ -261,8 +261,6 @@ export const MistralParseV2Block: BlockConfig<MistralParserOutput> = {
},
inputs: {
document: { type: 'json', description: 'Document input (file upload or URL reference)' },
filePath: { type: 'string', description: 'PDF document URL (advanced mode)' },
fileUpload: { type: 'json', description: 'Uploaded PDF file (basic mode)' },
apiKey: { type: 'string', description: 'Mistral API key' },
resultType: { type: 'string', description: 'Output format type' },
pages: { type: 'string', description: 'Page selection' },
@@ -345,11 +343,8 @@ export const MistralParseV3Block: BlockConfig<MistralParserOutput> = {
resultType: params.resultType || 'markdown',
}
// V3 pattern: normalize file inputs from basic/advanced modes
const documentInput = normalizeFileInput(
params.fileUpload || params.fileReference || params.document,
{ single: true }
)
// V3 pattern: use canonical document param directly
const documentInput = normalizeFileInput(params.document, { single: true })
if (!documentInput) {
throw new Error('PDF document is required')
}
@@ -389,8 +384,6 @@ export const MistralParseV3Block: BlockConfig<MistralParserOutput> = {
},
inputs: {
document: { type: 'json', description: 'Document input (file upload or file reference)' },
fileReference: { type: 'json', description: 'File reference (advanced mode)' },
fileUpload: { type: 'json', description: 'Uploaded PDF file (basic mode)' },
apiKey: { type: 'string', description: 'Mistral API key' },
resultType: { type: 'string', description: 'Output format type' },
pages: { type: 'string', description: 'Page selection' },

View File

@@ -367,7 +367,6 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
values,
downloadFileName,
file,
fileReference,
...rest
} = params
@@ -376,8 +375,8 @@ export const OneDriveBlock: BlockConfig<OneDriveResponse> = {
normalizedValues = normalizeExcelValuesForToolParams(values)
}
// Normalize file input from both basic (file-upload) and advanced (short-input) modes
const normalizedFile = normalizeFileInput(file || fileReference, { single: true })
// Normalize file input from the canonical param
const normalizedFile = normalizeFileInput(file, { single: true })
// Resolve folderId based on operation
let resolvedFolderId: string | undefined

View File

@@ -122,7 +122,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
},
// Variable reference (advanced mode)
{
id: 'attachments',
id: 'attachmentReference',
title: 'Attachments',
type: 'short-input',
canonicalParamId: 'attachments',
@@ -171,7 +171,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
},
// Read Email Fields - Add folder selector (basic mode)
{
id: 'folder',
id: 'folderSelector',
title: 'Folder',
type: 'folder-selector',
canonicalParamId: 'folder',
@@ -328,24 +328,20 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
const {
credential,
folder,
manualFolder,
destinationFolder,
manualDestinationFolder,
destinationId,
copyDestinationId,
attachments,
moveMessageId,
actionMessageId,
copyMessageId,
copyDestinationFolder,
manualCopyDestinationFolder,
attachmentFiles,
attachments,
...rest
} = params
// Handle both selector and manual folder input
const effectiveFolder = (folder || manualFolder || '').trim()
// folder is already the canonical param - use it directly
const effectiveFolder = folder ? String(folder).trim() : ''
// Normalize file attachments from either basic (file-upload) or advanced (short-input) mode
const normalizedAttachments = normalizeFileInput(attachmentFiles || attachments)
// Normalize file attachments from the canonical attachments param
const normalizedAttachments = normalizeFileInput(attachments)
if (normalizedAttachments) {
rest.attachments = normalizedAttachments
}
@@ -359,8 +355,10 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
if (moveMessageId) {
rest.messageId = moveMessageId
}
if (!rest.destinationId) {
rest.destinationId = (destinationFolder || manualDestinationFolder || '').trim()
// destinationId is already the canonical param
const effectiveDestinationId = destinationId ? String(destinationId).trim() : ''
if (effectiveDestinationId) {
rest.destinationId = effectiveDestinationId
}
}
@@ -376,12 +374,12 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
if (copyMessageId) {
rest.messageId = copyMessageId
}
// Handle copyDestinationId (from UI canonical param) or destinationId (from trigger)
if (rest.copyDestinationId) {
rest.destinationId = rest.copyDestinationId
rest.copyDestinationId = undefined
} else if (!rest.destinationId) {
rest.destinationId = (copyDestinationFolder || manualCopyDestinationFolder || '').trim()
// copyDestinationId is the canonical param - map it to destinationId for the tool
const effectiveCopyDestinationId = copyDestinationId
? String(copyDestinationId).trim()
: ''
if (effectiveCopyDestinationId) {
rest.destinationId = effectiveCopyDestinationId
}
}
@@ -400,30 +398,24 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
subject: { type: 'string', description: 'Email subject' },
body: { type: 'string', description: 'Email content' },
contentType: { type: 'string', description: 'Content type (Text or HTML)' },
attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' },
attachments: { type: 'array', description: 'Files to attach (UserFile array)' },
attachments: { type: 'array', description: 'Files to attach (canonical param)' },
// Forward operation inputs
messageId: { type: 'string', description: 'Message ID to forward' },
comment: { type: 'string', description: 'Optional comment for forwarding' },
// Read operation inputs
folder: { type: 'string', description: 'Email folder' },
manualFolder: { type: 'string', description: 'Manual folder name' },
folder: { type: 'string', description: 'Email folder (canonical param)' },
maxResults: { type: 'number', description: 'Maximum emails' },
includeAttachments: { type: 'boolean', description: 'Include email attachments' },
// Move operation inputs
moveMessageId: { type: 'string', description: 'Message ID to move' },
destinationFolder: { type: 'string', description: 'Destination folder ID' },
manualDestinationFolder: { type: 'string', description: 'Manual destination folder ID' },
destinationId: { type: 'string', description: 'Destination folder ID for move' },
destinationId: { type: 'string', description: 'Destination folder ID (canonical param)' },
// Action operation inputs
actionMessageId: { type: 'string', description: 'Message ID for actions' },
copyMessageId: { type: 'string', description: 'Message ID to copy' },
copyDestinationFolder: { type: 'string', description: 'Copy destination folder ID' },
manualCopyDestinationFolder: {
copyDestinationId: {
type: 'string',
description: 'Manual copy destination folder ID',
description: 'Destination folder ID for copy (canonical param)',
},
copyDestinationId: { type: 'string', description: 'Destination folder ID for copy' },
},
outputs: {
// Common outputs

View File

@@ -74,7 +74,8 @@ export const PulseBlock: BlockConfig<PulseParserOutput> = {
apiKey: params.apiKey.trim(),
}
const documentInput = params.fileUpload || params.filePath || params.document
// document is the canonical param from fileUpload (basic) or filePath (advanced)
const documentInput = params.document
if (!documentInput) {
throw new Error('Document is required')
}
@@ -104,9 +105,10 @@ export const PulseBlock: BlockConfig<PulseParserOutput> = {
},
},
inputs: {
document: { type: 'json', description: 'Document input (file upload or URL reference)' },
filePath: { type: 'string', description: 'Document URL (advanced mode)' },
fileUpload: { type: 'json', description: 'Uploaded document file (basic mode)' },
document: {
type: 'json',
description: 'Document input (canonical param for file upload or URL)',
},
apiKey: { type: 'string', description: 'Pulse API key' },
pages: { type: 'string', description: 'Page range selection' },
chunking: {
@@ -129,14 +131,8 @@ export const PulseBlock: BlockConfig<PulseParserOutput> = {
},
}
// PulseV2Block uses the same canonical param 'document' for both basic and advanced modes
const pulseV2Inputs = PulseBlock.inputs
? {
...Object.fromEntries(
Object.entries(PulseBlock.inputs).filter(([key]) => key !== 'filePath')
),
fileReference: { type: 'json', description: 'File reference (advanced mode)' },
}
: {}
const pulseV2SubBlocks = (PulseBlock.subBlocks || []).flatMap((subBlock) => {
if (subBlock.id === 'filePath') {
return [] // Remove the old filePath subblock
@@ -183,10 +179,8 @@ export const PulseV2Block: BlockConfig<PulseParserOutput> = {
apiKey: params.apiKey.trim(),
}
const normalizedFile = normalizeFileInput(
params.fileUpload || params.fileReference || params.document,
{ single: true }
)
// document is the canonical param from fileUpload (basic) or fileReference (advanced)
const normalizedFile = normalizeFileInput(params.document, { single: true })
if (!normalizedFile) {
throw new Error('Document file is required')
}

View File

@@ -70,7 +70,8 @@ export const ReductoBlock: BlockConfig<ReductoParserOutput> = {
apiKey: params.apiKey.trim(),
}
const documentInput = params.fileUpload || params.filePath || params.document
// document is the canonical param from fileUpload (basic) or filePath (advanced)
const documentInput = params.document
if (!documentInput) {
throw new Error('PDF document is required')
}
@@ -118,9 +119,10 @@ export const ReductoBlock: BlockConfig<ReductoParserOutput> = {
},
},
inputs: {
document: { type: 'json', description: 'Document input (file upload or URL reference)' },
filePath: { type: 'string', description: 'PDF document URL (advanced mode)' },
fileUpload: { type: 'json', description: 'Uploaded PDF file (basic mode)' },
document: {
type: 'json',
description: 'Document input (canonical param for file upload or URL)',
},
apiKey: { type: 'string', description: 'Reducto API key' },
pages: { type: 'string', description: 'Page selection' },
tableOutputFormat: { type: 'string', description: 'Table output format' },
@@ -135,14 +137,8 @@ export const ReductoBlock: BlockConfig<ReductoParserOutput> = {
},
}
// ReductoV2Block uses the same canonical param 'document' for both basic and advanced modes
const reductoV2Inputs = ReductoBlock.inputs
? {
...Object.fromEntries(
Object.entries(ReductoBlock.inputs).filter(([key]) => key !== 'filePath')
),
fileReference: { type: 'json', description: 'File reference (advanced mode)' },
}
: {}
const reductoV2SubBlocks = (ReductoBlock.subBlocks || []).flatMap((subBlock) => {
if (subBlock.id === 'filePath') {
return []
@@ -187,10 +183,8 @@ export const ReductoV2Block: BlockConfig<ReductoParserOutput> = {
apiKey: params.apiKey.trim(),
}
const documentInput = normalizeFileInput(
params.fileUpload || params.fileReference || params.document,
{ single: true }
)
// document is the canonical param from fileUpload (basic) or fileReference (advanced)
const documentInput = normalizeFileInput(params.document, { single: true })
if (!documentInput) {
throw new Error('PDF document file is required')
}

View File

@@ -87,7 +87,7 @@ export const S3Block: BlockConfig<S3Response> = {
multiple: false,
},
{
id: 'file',
id: 'fileReference',
title: 'File Reference',
type: 'short-input',
canonicalParamId: 'file',
@@ -216,7 +216,6 @@ export const S3Block: BlockConfig<S3Response> = {
placeholder: 'Select ACL for copied object (default: private)',
condition: { field: 'operation', value: 'copy_object' },
mode: 'advanced',
canonicalParamId: 'acl',
},
],
tools: {
@@ -271,9 +270,9 @@ export const S3Block: BlockConfig<S3Response> = {
if (!params.objectKey) {
throw new Error('Object Key is required for upload')
}
// Use file from uploadFile if in basic mode, otherwise use file reference
// file is the canonical param from uploadFile (basic) or fileReference (advanced)
// normalizeFileInput handles JSON stringified values from advanced mode
const fileParam = normalizeFileInput(params.uploadFile || params.file, { single: true })
const fileParam = normalizeFileInput(params.file, { single: true })
return {
accessKeyId: params.accessKeyId,
@@ -396,8 +395,7 @@ export const S3Block: BlockConfig<S3Response> = {
bucketName: { type: 'string', description: 'S3 bucket name' },
// Upload inputs
objectKey: { type: 'string', description: 'Object key/path in S3' },
uploadFile: { type: 'json', description: 'File to upload (UI)' },
file: { type: 'json', description: 'File to upload (reference)' },
file: { type: 'json', description: 'File to upload (canonical param)' },
content: { type: 'string', description: 'Text content to upload' },
contentType: { type: 'string', description: 'Content-Type header' },
acl: { type: 'string', description: 'Access control list' },

View File

@@ -562,13 +562,12 @@ Return ONLY the HTML content.`,
templateGenerations,
listPageSize,
templatePageSize,
attachmentFiles,
attachments,
...rest
} = params
// Normalize attachments for send_mail operation
const normalizedAttachments = normalizeFileInput(attachmentFiles || attachments)
const normalizedAttachments = normalizeFileInput(attachments)
// Map renamed fields back to tool parameter names
return {
@@ -606,8 +605,7 @@ Return ONLY the HTML content.`,
replyToName: { type: 'string', description: 'Reply-to name' },
mailTemplateId: { type: 'string', description: 'Template ID for sending mail' },
dynamicTemplateData: { type: 'json', description: 'Dynamic template data' },
attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' },
attachments: { type: 'array', description: 'Files to attach (UserFile array)' },
attachments: { type: 'array', description: 'Files to attach (canonical param)' },
// Contact inputs
email: { type: 'string', description: 'Contact email' },
firstName: { type: 'string', description: 'Contact first name' },

View File

@@ -223,7 +223,8 @@ export const SftpBlock: BlockConfig<SftpUploadResult> = {
return {
...connectionConfig,
remotePath: params.remotePath,
files: normalizeFileInput(params.uploadFiles || params.files),
// files is the canonical param from uploadFiles (basic) or files (advanced)
files: normalizeFileInput(params.files),
overwrite: params.overwrite !== false,
permissions: params.permissions,
}

View File

@@ -252,7 +252,19 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
placeholder: 'Enter site ID (leave empty for root site)',
dependsOn: ['credential'],
mode: 'advanced',
condition: { field: 'operation', value: 'create_page' },
condition: {
field: 'operation',
value: [
'create_page',
'read_page',
'list_sites',
'create_list',
'read_list',
'update_list',
'add_list_items',
'upload_file',
],
},
},
{
@@ -391,18 +403,17 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
}
},
params: (params) => {
const { credential, siteSelector, manualSiteId, mimeType, ...rest } = params
const { credential, siteId, mimeType, ...rest } = params
const effectiveSiteId = (siteSelector || manualSiteId || '').trim()
// siteId is the canonical param from siteSelector (basic) or manualSiteId (advanced)
const effectiveSiteId = siteId ? String(siteId).trim() : ''
const {
itemId: providedItemId,
listItemId,
listItemFields,
itemId, // canonical param from listItemId
listItemFields, // canonical param
includeColumns,
includeItems,
uploadFiles,
files,
files, // canonical param from uploadFiles (basic) or files (advanced)
columnDefinitions,
...others
} = rest as any
@@ -421,11 +432,9 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
parsedItemFields = undefined
}
const rawItemId = providedItemId ?? listItemId
// itemId is the canonical param from listItemId
const sanitizedItemId =
rawItemId === undefined || rawItemId === null
? undefined
: String(rawItemId).trim() || undefined
itemId === undefined || itemId === null ? undefined : String(itemId).trim() || undefined
const coerceBoolean = (value: any) => {
if (typeof value === 'boolean') return value
@@ -449,8 +458,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
} catch {}
}
// Handle file upload files parameter
const normalizedFiles = normalizeFileInput(uploadFiles || files)
// Handle file upload files parameter using canonical param
const normalizedFiles = normalizeFileInput(files)
const baseParams: Record<string, any> = {
credential,
siteId: effectiveSiteId || undefined,
@@ -486,8 +495,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
},
pageTitle: { type: 'string', description: 'Page title' },
pageId: { type: 'string', description: 'Page ID' },
siteSelector: { type: 'string', description: 'Site selector' },
manualSiteId: { type: 'string', description: 'Manual site ID' },
siteId: { type: 'string', description: 'Site ID' },
pageSize: { type: 'number', description: 'Results per page' },
listDisplayName: { type: 'string', description: 'List display name' },
listDescription: { type: 'string', description: 'List description' },
@@ -496,13 +504,12 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
listTitle: { type: 'string', description: 'List title' },
includeColumns: { type: 'boolean', description: 'Include columns in response' },
includeItems: { type: 'boolean', description: 'Include items in response' },
listItemId: { type: 'string', description: 'List item ID' },
listItemFields: { type: 'string', description: 'List item fields' },
driveId: { type: 'string', description: 'Document library (drive) ID' },
itemId: { type: 'string', description: 'List item ID (canonical param)' },
listItemFields: { type: 'string', description: 'List item fields (canonical param)' },
driveId: { type: 'string', description: 'Document library (drive) ID (canonical param)' },
folderPath: { type: 'string', description: 'Folder path for file upload' },
fileName: { type: 'string', description: 'File name override' },
uploadFiles: { type: 'json', description: 'Files to upload (UI upload)' },
files: { type: 'array', description: 'Files to upload (UserFile array)' },
files: { type: 'array', description: 'Files to upload (canonical param)' },
},
outputs: {
sites: {

View File

@@ -547,15 +547,12 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
operation,
destinationType,
channel,
manualChannel,
dmUserId,
manualDmUserId,
text,
title,
content,
limit,
oldest,
attachmentFiles,
files,
threadTs,
updateTimestamp,
@@ -576,8 +573,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
} = params
const isDM = destinationType === 'dm'
const effectiveChannel = (channel || manualChannel || '').trim()
const effectiveUserId = (dmUserId || manualDmUserId || '').trim()
const effectiveChannel = channel ? String(channel).trim() : ''
const effectiveUserId = dmUserId ? String(dmUserId).trim() : ''
const noChannelOperations = ['list_channels', 'list_users', 'get_user']
const dmSupportedOperations = ['send', 'read']
@@ -621,7 +618,8 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
if (threadTs) {
baseParams.thread_ts = threadTs
}
const normalizedFiles = normalizeFileInput(attachmentFiles || files)
// files is the canonical param from attachmentFiles (basic) or files (advanced)
const normalizedFiles = normalizeFileInput(files)
if (normalizedFiles) {
baseParams.files = normalizedFiles
}
@@ -741,19 +739,16 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
destinationType: { type: 'string', description: 'Destination type (channel or dm)' },
credential: { type: 'string', description: 'Slack access token' },
botToken: { type: 'string', description: 'Bot token' },
channel: { type: 'string', description: 'Channel identifier' },
manualChannel: { type: 'string', description: 'Manual channel identifier' },
dmUserId: { type: 'string', description: 'User ID for DM recipient (selector)' },
manualDmUserId: { type: 'string', description: 'User ID for DM recipient (manual input)' },
channel: { type: 'string', description: 'Channel identifier (canonical param)' },
dmUserId: { type: 'string', description: 'User ID for DM recipient (canonical param)' },
text: { type: 'string', description: 'Message text' },
attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' },
files: { type: 'array', description: 'Files to attach (UserFile array)' },
files: { type: 'array', description: 'Files to attach (canonical param)' },
title: { type: 'string', description: 'Canvas title' },
content: { type: 'string', description: 'Canvas content' },
limit: { type: 'string', description: 'Message limit' },
oldest: { type: 'string', description: 'Oldest timestamp' },
fileId: { type: 'string', description: 'File ID to download' },
downloadFileName: { type: 'string', description: 'File name override for download' },
fileName: { type: 'string', description: 'File name override for download (canonical param)' },
// Update/Delete/React operation inputs
updateTimestamp: { type: 'string', description: 'Message timestamp for update' },
updateText: { type: 'string', description: 'New text for update' },

View File

@@ -177,7 +177,7 @@ export const SmtpBlock: BlockConfig<SmtpSendMailResult> = {
cc: params.cc,
bcc: params.bcc,
replyTo: params.replyTo,
attachments: normalizeFileInput(params.attachmentFiles || params.attachments),
attachments: normalizeFileInput(params.attachments),
}),
},
},

View File

@@ -824,8 +824,6 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
description: { type: 'string', description: 'Playlist description' },
public: { type: 'boolean', description: 'Whether playlist is public' },
coverImage: { type: 'json', description: 'Cover image (UserFile)' },
coverImageFile: { type: 'json', description: 'Cover image upload (basic mode)' },
coverImageRef: { type: 'json', description: 'Cover image reference (advanced mode)' },
range_start: { type: 'number', description: 'Start index for reorder' },
insert_before: { type: 'number', description: 'Insert before index' },
range_length: { type: 'number', description: 'Number of items to move' },

View File

@@ -259,8 +259,8 @@ export const SttBlock: BlockConfig<SttBlockResponse> = {
}
},
params: (params) => {
// Normalize file input from basic (file-upload) or advanced (short-input) mode
const audioFile = normalizeFileInput(params.audioFile || params.audioFileReference, {
// Normalize file input - audioFile is the canonical param for both basic and advanced modes
const audioFile = normalizeFileInput(params.audioFile, {
single: true,
})
@@ -269,7 +269,6 @@ export const SttBlock: BlockConfig<SttBlockResponse> = {
apiKey: params.apiKey,
model: params.model,
audioFile,
audioFileReference: undefined,
audioUrl: params.audioUrl,
language: params.language,
timestamps: params.timestamps,
@@ -296,7 +295,6 @@ export const SttBlock: BlockConfig<SttBlockResponse> = {
'Provider-specific model (e.g., scribe_v1 for ElevenLabs, nova-3 for Deepgram, best for AssemblyAI, gemini-2.0-flash-exp for Gemini)',
},
audioFile: { type: 'json', description: 'Audio/video file (UserFile)' },
audioFileReference: { type: 'json', description: 'Audio/video file reference' },
audioUrl: { type: 'string', description: 'Audio/video URL' },
language: { type: 'string', description: 'Language code or auto' },
timestamps: { type: 'string', description: 'Timestamp granularity (none, sentence, word)' },
@@ -393,8 +391,8 @@ export const SttV2Block: BlockConfig<SttBlockResponse> = {
fallbackToolId: 'stt_whisper_v2',
}),
params: (params) => {
// Normalize file input from basic (file-upload) or advanced (short-input) mode
const audioFile = normalizeFileInput(params.audioFile || params.audioFileReference, {
// Normalize file input - audioFile is the canonical param for both basic and advanced modes
const audioFile = normalizeFileInput(params.audioFile, {
single: true,
})
@@ -403,7 +401,6 @@ export const SttV2Block: BlockConfig<SttBlockResponse> = {
apiKey: params.apiKey,
model: params.model,
audioFile,
audioFileReference: undefined,
language: params.language,
timestamps: params.timestamps,
diarization: params.diarization,

View File

@@ -974,15 +974,13 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
allowedMimeTypes,
upsert,
download,
file,
fileContent,
fileData,
...rest
} = params
// Normalize file input for storage_upload operation
// normalizeFileInput handles JSON stringified values from advanced mode
const normalizedFileData = normalizeFileInput(file || fileContent || fileData, {
// fileData is the canonical param for both basic (file) and advanced (fileContent) modes
const normalizedFileData = normalizeFileInput(fileData, {
single: true,
})
@@ -1156,7 +1154,7 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
// Storage operation inputs
bucket: { type: 'string', description: 'Storage bucket name' },
path: { type: 'string', description: 'File or folder path in storage' },
fileContent: { type: 'string', description: 'File content (base64 for binary)' },
fileData: { type: 'json', description: 'File data (UserFile)' },
contentType: { type: 'string', description: 'MIME type of the file' },
fileName: { type: 'string', description: 'File name for upload or download override' },
upsert: { type: 'boolean', description: 'Whether to overwrite existing file' },

View File

@@ -269,7 +269,8 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
messageId: params.messageId,
}
case 'telegram_send_photo': {
const photoSource = normalizeFileInput(params.photoFile || params.photo, {
// photo is the canonical param for both basic (photoFile) and advanced modes
const photoSource = normalizeFileInput(params.photo, {
single: true,
})
if (!photoSource) {
@@ -282,7 +283,8 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
}
}
case 'telegram_send_video': {
const videoSource = normalizeFileInput(params.videoFile || params.video, {
// video is the canonical param for both basic (videoFile) and advanced modes
const videoSource = normalizeFileInput(params.video, {
single: true,
})
if (!videoSource) {
@@ -295,7 +297,8 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
}
}
case 'telegram_send_audio': {
const audioSource = normalizeFileInput(params.audioFile || params.audio, {
// audio is the canonical param for both basic (audioFile) and advanced modes
const audioSource = normalizeFileInput(params.audio, {
single: true,
})
if (!audioSource) {
@@ -308,7 +311,8 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
}
}
case 'telegram_send_animation': {
const animationSource = normalizeFileInput(params.animationFile || params.animation, {
// animation is the canonical param for both basic (animationFile) and advanced modes
const animationSource = normalizeFileInput(params.animation, {
single: true,
})
if (!animationSource) {
@@ -321,9 +325,10 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
}
}
case 'telegram_send_document': {
// files is the canonical param for both basic (attachmentFiles) and advanced modes
return {
...commonParams,
files: normalizeFileInput(params.attachmentFiles || params.files),
files: normalizeFileInput(params.files),
caption: params.caption,
}
}
@@ -341,18 +346,10 @@ export const TelegramBlock: BlockConfig<TelegramResponse> = {
botToken: { type: 'string', description: 'Telegram bot token' },
chatId: { type: 'string', description: 'Chat identifier' },
text: { type: 'string', description: 'Message text' },
photoFile: { type: 'json', description: 'Uploaded photo (UserFile)' },
photo: { type: 'json', description: 'Photo reference or URL/file_id' },
videoFile: { type: 'json', description: 'Uploaded video (UserFile)' },
video: { type: 'json', description: 'Video reference or URL/file_id' },
audioFile: { type: 'json', description: 'Uploaded audio (UserFile)' },
audio: { type: 'json', description: 'Audio reference or URL/file_id' },
animationFile: { type: 'json', description: 'Uploaded animation (UserFile)' },
animation: { type: 'json', description: 'Animation reference or URL/file_id' },
attachmentFiles: {
type: 'json',
description: 'Files to attach (UI upload)',
},
photo: { type: 'json', description: 'Photo (UserFile or URL/file_id)' },
video: { type: 'json', description: 'Video (UserFile or URL/file_id)' },
audio: { type: 'json', description: 'Audio (UserFile or URL/file_id)' },
animation: { type: 'json', description: 'Animation (UserFile or URL/file_id)' },
files: { type: 'array', description: 'Files to attach (UserFile array)' },
caption: { type: 'string', description: 'Caption for media' },
messageId: { type: 'string', description: 'Message ID to delete' },

View File

@@ -137,7 +137,8 @@ export const TextractBlock: BlockConfig<TextractParserOutput> = {
}
parameters.s3Uri = params.s3Uri.trim()
} else {
const documentInput = params.fileUpload || params.filePath || params.document
// document is the canonical param for both basic (fileUpload) and advanced (filePath) modes
const documentInput = params.document
if (!documentInput) {
throw new Error('Document is required')
}
@@ -165,8 +166,6 @@ export const TextractBlock: BlockConfig<TextractParserOutput> = {
inputs: {
processingMode: { type: 'string', description: 'Document type: single-page or multi-page' },
document: { type: 'json', description: 'Document input (file upload or URL reference)' },
filePath: { type: 'string', description: 'Document URL (advanced mode)' },
fileUpload: { type: 'json', description: 'Uploaded document file (basic mode)' },
s3Uri: { type: 'string', description: 'S3 URI for multi-page processing (s3://bucket/key)' },
extractTables: { type: 'boolean', description: 'Extract tables from document' },
extractForms: { type: 'boolean', description: 'Extract form key-value pairs' },
@@ -192,14 +191,7 @@ export const TextractBlock: BlockConfig<TextractParserOutput> = {
},
}
const textractV2Inputs = TextractBlock.inputs
? {
...Object.fromEntries(
Object.entries(TextractBlock.inputs).filter(([key]) => key !== 'filePath')
),
fileReference: { type: 'json', description: 'File reference (advanced mode)' },
}
: {}
const textractV2Inputs = TextractBlock.inputs ? { ...TextractBlock.inputs } : {}
const textractV2SubBlocks = (TextractBlock.subBlocks || []).flatMap((subBlock) => {
if (subBlock.id === 'filePath') {
return [] // Remove the old filePath subblock
@@ -265,10 +257,8 @@ export const TextractV2Block: BlockConfig<TextractParserOutput> = {
}
parameters.s3Uri = params.s3Uri.trim()
} else {
const file = normalizeFileInput(
params.fileUpload || params.fileReference || params.document,
{ single: true }
)
// document is the canonical param for both basic (fileUpload) and advanced (fileReference) modes
const file = normalizeFileInput(params.document, { single: true })
if (!file) {
throw new Error('Document file is required')
}

View File

@@ -691,6 +691,7 @@ export const VideoGeneratorV2Block: BlockConfig<VideoBlockResponse> = {
condition: { field: 'provider', value: 'runway' },
placeholder: 'Reference image from previous blocks',
mode: 'advanced',
required: true,
},
{
id: 'cameraControl',
@@ -734,29 +735,25 @@ export const VideoGeneratorV2Block: BlockConfig<VideoBlockResponse> = {
return 'video_runway'
}
},
params: (params) => {
const visualRef =
params.visualReferenceUpload || params.visualReferenceInput || params.visualReference
return {
provider: params.provider,
apiKey: params.apiKey,
model: params.model,
endpoint: params.endpoint,
prompt: params.prompt,
duration: params.duration ? Number(params.duration) : undefined,
aspectRatio: params.aspectRatio,
resolution: params.resolution,
visualReference: normalizeFileInput(visualRef, { single: true }),
consistencyMode: params.consistencyMode,
stylePreset: params.stylePreset,
promptOptimizer: params.promptOptimizer,
cameraControl: params.cameraControl
? typeof params.cameraControl === 'string'
? JSON.parse(params.cameraControl)
: params.cameraControl
: undefined,
}
},
params: (params) => ({
provider: params.provider,
apiKey: params.apiKey,
model: params.model,
endpoint: params.endpoint,
prompt: params.prompt,
duration: params.duration ? Number(params.duration) : undefined,
aspectRatio: params.aspectRatio,
resolution: params.resolution,
visualReference: normalizeFileInput(params.visualReference, { single: true }),
consistencyMode: params.consistencyMode,
stylePreset: params.stylePreset,
promptOptimizer: params.promptOptimizer,
cameraControl: params.cameraControl
? typeof params.cameraControl === 'string'
? JSON.parse(params.cameraControl)
: params.cameraControl
: undefined,
}),
},
},
inputs: {
@@ -784,11 +781,6 @@ export const VideoGeneratorV2Block: BlockConfig<VideoBlockResponse> = {
description: 'Video resolution - not available for MiniMax (fixed per endpoint)',
},
visualReference: { type: 'json', description: 'Reference image for Runway (UserFile)' },
visualReferenceUpload: { type: 'json', description: 'Uploaded reference image (basic mode)' },
visualReferenceInput: {
type: 'json',
description: 'Reference image from previous blocks (advanced mode)',
},
consistencyMode: {
type: 'string',
description: 'Consistency mode for Runway (character, object, style, location)',

View File

@@ -91,7 +91,6 @@ export const VisionBlock: BlockConfig<VisionResponse> = {
apiKey: { type: 'string', description: 'Provider API key' },
imageUrl: { type: 'string', description: 'Image URL' },
imageFile: { type: 'json', description: 'Image file (UserFile)' },
imageFileReference: { type: 'json', description: 'Image file reference' },
model: { type: 'string', description: 'Vision model' },
prompt: { type: 'string', description: 'Analysis prompt' },
},
@@ -117,15 +116,13 @@ export const VisionV2Block: BlockConfig<VisionResponse> = {
fallbackToolId: 'vision_tool_v2',
}),
params: (params) => {
// normalizeFileInput handles JSON stringified values from advanced mode
// Vision expects a single file
const imageFile = normalizeFileInput(params.imageFile || params.imageFileReference, {
// imageFile is the canonical param for both basic and advanced modes
const imageFile = normalizeFileInput(params.imageFile, {
single: true,
})
return {
...params,
imageFile,
imageFileReference: undefined,
}
},
},
@@ -177,7 +174,6 @@ export const VisionV2Block: BlockConfig<VisionResponse> = {
inputs: {
apiKey: { type: 'string', description: 'Provider API key' },
imageFile: { type: 'json', description: 'Image file (UserFile)' },
imageFileReference: { type: 'json', description: 'Image file reference' },
model: { type: 'string', description: 'Vision model' },
prompt: { type: 'string', description: 'Analysis prompt' },
},

View File

@@ -169,9 +169,10 @@ Return ONLY the date/time string - no explanations, no quotes, no extra text.`,
}
},
params: (params) => {
const { credential, operation, contactId, manualContactId, taskId, ...rest } = params
const { credential, operation, contactId, taskId, ...rest } = params
const effectiveContactId = (contactId || manualContactId || '').trim()
// contactId is the canonical param for both basic (file-selector) and advanced (manualContactId) modes
const effectiveContactId = contactId ? String(contactId).trim() : ''
const baseParams = {
...rest,
@@ -222,7 +223,6 @@ Return ONLY the date/time string - no explanations, no quotes, no extra text.`,
credential: { type: 'string', description: 'Wealthbox access token' },
noteId: { type: 'string', description: 'Note identifier' },
contactId: { type: 'string', description: 'Contact identifier' },
manualContactId: { type: 'string', description: 'Manual contact identifier' },
taskId: { type: 'string', description: 'Task identifier' },
content: { type: 'string', description: 'Content text' },
firstName: { type: 'string', description: 'First name' },

View File

@@ -40,7 +40,7 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
required: true,
},
{
id: 'siteId',
id: 'siteSelector',
title: 'Site',
type: 'project-selector',
canonicalParamId: 'siteId',
@@ -60,13 +60,13 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
required: true,
},
{
id: 'collectionId',
id: 'collectionSelector',
title: 'Collection',
type: 'file-selector',
canonicalParamId: 'collectionId',
serviceId: 'webflow',
placeholder: 'Select collection',
dependsOn: ['credential', 'siteId'],
dependsOn: ['credential', 'siteSelector'],
mode: 'basic',
required: true,
},
@@ -80,13 +80,13 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
required: true,
},
{
id: 'itemId',
id: 'itemSelector',
title: 'Item',
type: 'file-selector',
canonicalParamId: 'itemId',
serviceId: 'webflow',
placeholder: 'Select item',
dependsOn: ['credential', 'collectionId'],
dependsOn: ['credential', 'collectionSelector'],
mode: 'basic',
condition: { field: 'operation', value: ['get', 'update', 'delete'] },
required: true,
@@ -158,12 +158,9 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
const {
credential,
fieldData,
siteId,
manualSiteId,
collectionId,
manualCollectionId,
itemId,
manualItemId,
siteId, // Canonical param from siteSelector (basic) or manualSiteId (advanced)
collectionId, // Canonical param from collectionSelector (basic) or manualCollectionId (advanced)
itemId, // Canonical param from itemSelector (basic) or manualItemId (advanced)
...rest
} = params
let parsedFieldData: any | undefined
@@ -176,13 +173,9 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
throw new Error(`Invalid JSON input for ${params.operation} operation: ${error.message}`)
}
const effectiveSiteId = ((siteId as string) || (manualSiteId as string) || '').trim()
const effectiveCollectionId = (
(collectionId as string) ||
(manualCollectionId as string) ||
''
).trim()
const effectiveItemId = ((itemId as string) || (manualItemId as string) || '').trim()
const effectiveSiteId = siteId ? String(siteId).trim() : ''
const effectiveCollectionId = collectionId ? String(collectionId).trim() : ''
const effectiveItemId = itemId ? String(itemId).trim() : ''
if (!effectiveSiteId) {
throw new Error('Site ID is required')
@@ -226,11 +219,8 @@ export const WebflowBlock: BlockConfig<WebflowResponse> = {
operation: { type: 'string', description: 'Operation to perform' },
credential: { type: 'string', description: 'Webflow OAuth access token' },
siteId: { type: 'string', description: 'Webflow site identifier' },
manualSiteId: { type: 'string', description: 'Manual site identifier' },
collectionId: { type: 'string', description: 'Webflow collection identifier' },
manualCollectionId: { type: 'string', description: 'Manual collection identifier' },
itemId: { type: 'string', description: 'Item identifier' },
manualItemId: { type: 'string', description: 'Manual item identifier' },
offset: { type: 'number', description: 'Pagination offset' },
limit: { type: 'number', description: 'Maximum items to return' },
fieldData: { type: 'json', description: 'Item field data' },

View File

@@ -768,9 +768,10 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
parent: params.parent ? Number(params.parent) : undefined,
}
case 'wordpress_upload_media':
// file is the canonical param for both basic (fileUpload) and advanced modes
return {
...baseParams,
file: normalizeFileInput(params.fileUpload || params.file, { single: true }),
file: normalizeFileInput(params.file, { single: true }),
filename: params.filename,
title: params.mediaTitle,
caption: params.caption,
@@ -905,8 +906,7 @@ export const WordPressBlock: BlockConfig<WordPressResponse> = {
parent: { type: 'number', description: 'Parent page ID' },
menuOrder: { type: 'number', description: 'Menu order' },
// Media inputs
fileUpload: { type: 'json', description: 'File to upload (UserFile object)' },
file: { type: 'json', description: 'File reference from previous block' },
file: { type: 'json', description: 'File to upload (UserFile)' },
filename: { type: 'string', description: 'Optional filename override' },
mediaTitle: { type: 'string', description: 'Media title' },
caption: { type: 'string', description: 'Media caption' },

View File

@@ -42,7 +42,7 @@ export const googleFormsWebhookTrigger: TriggerConfig = {
mode: 'trigger',
},
{
id: 'formId',
id: 'triggerFormId',
title: 'Form ID',
type: 'short-input',
placeholder: '1FAIpQLSd... (Google Form ID)',

View File

@@ -47,7 +47,7 @@ export const microsoftTeamsChatSubscriptionTrigger: TriggerConfig = {
},
},
{
id: 'chatId',
id: 'triggerChatId',
title: 'Chat ID',
type: 'short-input',
placeholder: 'Enter chat ID',

View File

@@ -30,7 +30,7 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
},
},
{
id: 'siteId',
id: 'triggerSiteId',
title: 'Site',
type: 'dropdown',
placeholder: 'Select a site',
@@ -96,7 +96,7 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
dependsOn: ['triggerCredentials'],
},
{
id: 'collectionId',
id: 'triggerCollectionId',
title: 'Collection',
type: 'dropdown',
placeholder: 'Select a collection (optional)',
@@ -112,7 +112,9 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
const siteId = useSubBlockStore.getState().getValue(blockId, 'triggerSiteId') as
| string
| null
if (!credentialId || !siteId) {
return []
}
@@ -142,7 +144,9 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
const siteId = useSubBlockStore.getState().getValue(blockId, 'triggerSiteId') as
| string
| null
if (!credentialId || !siteId) return null
try {
const response = await fetch('/api/tools/webflow/collections', {
@@ -161,7 +165,7 @@ export const webflowCollectionItemChangedTrigger: TriggerConfig = {
return null
}
},
dependsOn: ['triggerCredentials', 'siteId'],
dependsOn: ['triggerCredentials', 'triggerSiteId'],
},
{
id: 'triggerSave',

View File

@@ -44,7 +44,7 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
},
},
{
id: 'siteId',
id: 'triggerSiteId',
title: 'Site',
type: 'dropdown',
placeholder: 'Select a site',
@@ -110,7 +110,7 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
dependsOn: ['triggerCredentials'],
},
{
id: 'collectionId',
id: 'triggerCollectionId',
title: 'Collection',
type: 'dropdown',
placeholder: 'Select a collection (optional)',
@@ -126,7 +126,9 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
const siteId = useSubBlockStore.getState().getValue(blockId, 'triggerSiteId') as
| string
| null
if (!credentialId || !siteId) {
return []
}
@@ -156,7 +158,9 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
const siteId = useSubBlockStore.getState().getValue(blockId, 'triggerSiteId') as
| string
| null
if (!credentialId || !siteId) return null
try {
const response = await fetch('/api/tools/webflow/collections', {
@@ -175,7 +179,7 @@ export const webflowCollectionItemCreatedTrigger: TriggerConfig = {
return null
}
},
dependsOn: ['triggerCredentials', 'siteId'],
dependsOn: ['triggerCredentials', 'triggerSiteId'],
},
{
id: 'triggerSave',

View File

@@ -30,7 +30,7 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
},
},
{
id: 'siteId',
id: 'triggerSiteId',
title: 'Site',
type: 'dropdown',
placeholder: 'Select a site',
@@ -96,7 +96,7 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
dependsOn: ['triggerCredentials'],
},
{
id: 'collectionId',
id: 'triggerCollectionId',
title: 'Collection',
type: 'dropdown',
placeholder: 'Select a collection (optional)',
@@ -112,7 +112,9 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
const siteId = useSubBlockStore.getState().getValue(blockId, 'triggerSiteId') as
| string
| null
if (!credentialId || !siteId) {
return []
}
@@ -142,7 +144,9 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
const credentialId = useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as
| string
| null
const siteId = useSubBlockStore.getState().getValue(blockId, 'siteId') as string | null
const siteId = useSubBlockStore.getState().getValue(blockId, 'triggerSiteId') as
| string
| null
if (!credentialId || !siteId) return null
try {
const response = await fetch('/api/tools/webflow/collections', {
@@ -161,7 +165,7 @@ export const webflowCollectionItemDeletedTrigger: TriggerConfig = {
return null
}
},
dependsOn: ['triggerCredentials', 'siteId'],
dependsOn: ['triggerCredentials', 'triggerSiteId'],
},
{
id: 'triggerSave',

View File

@@ -30,7 +30,7 @@ export const webflowFormSubmissionTrigger: TriggerConfig = {
},
},
{
id: 'siteId',
id: 'triggerSiteId',
title: 'Site',
type: 'dropdown',
placeholder: 'Select a site',