fix(docs): resolve missing tool outputs for spread-inherited V2 tools (#4020)

* fix(docs): resolve missing tool outputs for spread-inherited V2 tools

* fix(docs): add word boundary to baseToolRegex to prevent false matches

* fix(docs): remove unnecessary case-insensitive flag from baseToolRegex
This commit is contained in:
Waleed
2026-04-07 12:41:23 -07:00
committed by GitHub
parent c89a95d606
commit 762fbbd3e2
8 changed files with 320 additions and 283 deletions

View File

@@ -53,6 +53,27 @@ interface BlockConfig {
[key: string]: any
}
/**
* Find the position after the matching close delimiter for an opening delimiter.
* Assumes `content[openPos]` is the opening char (e.g. `{` or `[`).
* Returns the index one past the matching close char, or -1 if unbalanced.
*/
function findMatchingClose(
content: string,
openPos: number,
openChar = '{',
closeChar = '}'
): number {
let count = 1
let pos = openPos + 1
while (pos < content.length && count > 0) {
if (content[pos] === openChar) count++
else if (content[pos] === closeChar) count--
pos++
}
return count === 0 ? pos : -1
}
interface TriggerInfo {
id: string
name: string
@@ -134,16 +155,9 @@ async function generateIconMapping(): Promise<Record<string, string>> {
const startIndex = match.index + match[0].length - 1
// Extract the block content
let braceCount = 1
let endIndex = startIndex + 1
const endIndex = findMatchingClose(fileContent, startIndex)
while (endIndex < fileContent.length && braceCount > 0) {
if (fileContent[endIndex] === '{') braceCount++
else if (fileContent[endIndex] === '}') braceCount--
endIndex++
}
if (braceCount === 0) {
if (endIndex !== -1) {
const blockContent = fileContent.substring(startIndex, endIndex)
// Check hideFromToolbar - skip hidden blocks for docs but NOT for icon mapping
@@ -272,26 +286,16 @@ function extractOperationsFromContent(blockContent: string): { label: string; id
// Locate the opening '[' of the subBlocks array
const arrayStart = subBlocksMatch.index + subBlocksMatch[0].length - 1
let bracketCount = 1
let pos = arrayStart + 1
while (pos < blockContent.length && bracketCount > 0) {
if (blockContent[pos] === '[') bracketCount++
else if (blockContent[pos] === ']') bracketCount--
pos++
}
const subBlocksContent = blockContent.substring(arrayStart + 1, pos - 1)
const arrayEnd = findMatchingClose(blockContent, arrayStart, '[', ']')
if (arrayEnd === -1) return []
const subBlocksContent = blockContent.substring(arrayStart + 1, arrayEnd - 1)
// Iterate over top-level objects in the subBlocks array, looking for id: 'operation'
let i = 0
while (i < subBlocksContent.length) {
if (subBlocksContent[i] === '{') {
let braceCount = 1
let j = i + 1
while (j < subBlocksContent.length && braceCount > 0) {
if (subBlocksContent[j] === '{') braceCount++
else if (subBlocksContent[j] === '}') braceCount--
j++
}
const j = findMatchingClose(subBlocksContent, i)
if (j === -1) break
const objContent = subBlocksContent.substring(i, j)
if (/\bid\s*:\s*['"]operation['"]/.test(objContent)) {
@@ -299,14 +303,9 @@ function extractOperationsFromContent(blockContent: string): { label: string; id
if (!optionsMatch) return []
const optArrayStart = optionsMatch.index + optionsMatch[0].length - 1
let bc = 1
let op = optArrayStart + 1
while (op < objContent.length && bc > 0) {
if (objContent[op] === '[') bc++
else if (objContent[op] === ']') bc--
op++
}
const optionsContent = objContent.substring(optArrayStart + 1, op - 1)
const optArrayEnd = findMatchingClose(objContent, optArrayStart, '[', ']')
if (optArrayEnd === -1) return []
const optionsContent = objContent.substring(optArrayStart + 1, optArrayEnd - 1)
// Extract { label, id } pairs from each option object
const pairs: { label: string; id: string }[] = []
@@ -442,14 +441,9 @@ function extractTriggersAvailable(blockContent: string): string[] {
if (!triggersMatch) return []
const start = triggersMatch.index + triggersMatch[0].length - 1
let braceCount = 1
let pos = start + 1
while (pos < blockContent.length && braceCount > 0) {
if (blockContent[pos] === '{') braceCount++
else if (blockContent[pos] === '}') braceCount--
pos++
}
const triggersContent = blockContent.substring(start, pos)
const trigEnd = findMatchingClose(blockContent, start)
if (trigEnd === -1) return []
const triggersContent = blockContent.substring(start, trigEnd)
if (!/enabled\s*:\s*true/.test(triggersContent)) return []
@@ -457,14 +451,9 @@ function extractTriggersAvailable(blockContent: string): string[] {
if (!availableMatch) return []
const arrayStart = availableMatch.index + availableMatch[0].length - 1
let bracketCount = 1
let ap = arrayStart + 1
while (ap < triggersContent.length && bracketCount > 0) {
if (triggersContent[ap] === '[') bracketCount++
else if (triggersContent[ap] === ']') bracketCount--
ap++
}
const arrayContent = triggersContent.substring(arrayStart + 1, ap - 1)
const arrayEnd = findMatchingClose(triggersContent, arrayStart, '[', ']')
if (arrayEnd === -1) return []
const arrayContent = triggersContent.substring(arrayStart + 1, arrayEnd - 1)
const ids: string[] = []
const idRegex = /['"]([^'"]+)['"]/g
@@ -731,16 +720,9 @@ function extractAllBlockConfigs(fileContent: string): BlockConfig[] {
const startIndex = match.index + match[0].length - 1 // Position of opening brace
// Extract the block content by matching braces
let braceCount = 1
let endIndex = startIndex + 1
const endIndex = findMatchingClose(fileContent, startIndex)
while (endIndex < fileContent.length && braceCount > 0) {
if (fileContent[endIndex] === '{') braceCount++
else if (fileContent[endIndex] === '}') braceCount--
endIndex++
}
if (braceCount === 0) {
if (endIndex !== -1) {
const blockContent = fileContent.substring(startIndex, endIndex)
// Check if this block has hideFromToolbar: true
@@ -798,16 +780,9 @@ function extractBlockConfigFromContent(
if (baseMatch) {
const startIndex = baseMatch.index + baseMatch[0].length - 1
let braceCount = 1
let endIndex = startIndex + 1
const endIndex = findMatchingClose(fileContent, startIndex)
while (endIndex < fileContent.length && braceCount > 0) {
if (fileContent[endIndex] === '{') braceCount++
else if (fileContent[endIndex] === '}') braceCount--
endIndex++
}
if (braceCount === 0) {
if (endIndex !== -1) {
const baseBlockContent = fileContent.substring(startIndex, endIndex)
// Recursively extract base config (but don't pass fileContent to avoid infinite loops)
baseConfig = extractBlockConfigFromContent(
@@ -1015,62 +990,42 @@ function extractOutputsFromContent(content: string): Record<string, any> {
const openBracePos = content.indexOf('{', outputsStart)
if (openBracePos === -1) return {}
let braceCount = 1
let pos = openBracePos + 1
const pos = findMatchingClose(content, openBracePos)
if (pos === -1) return {}
while (pos < content.length && braceCount > 0) {
if (content[pos] === '{') braceCount++
else if (content[pos] === '}') braceCount--
pos++
const outputsContent = content.substring(openBracePos + 1, pos - 1).trim()
const outputs: Record<string, any> = {}
const fieldRegex = /(\w+)\s*:\s*{/g
let match
const fieldPositions: Array<{ name: string; start: number }> = []
while ((match = fieldRegex.exec(outputsContent)) !== null) {
fieldPositions.push({
name: match[1],
start: match.index + match[0].length - 1,
})
}
if (braceCount === 0) {
const outputsContent = content.substring(openBracePos + 1, pos - 1).trim()
const outputs: Record<string, any> = {}
fieldPositions.forEach((field) => {
const endPos = findMatchingClose(outputsContent, field.start)
const fieldRegex = /(\w+)\s*:\s*{/g
let match
const fieldPositions: Array<{ name: string; start: number }> = []
if (endPos !== -1) {
const fieldContent = outputsContent.substring(field.start + 1, endPos - 1).trim()
while ((match = fieldRegex.exec(outputsContent)) !== null) {
fieldPositions.push({
name: match[1],
start: match.index + match[0].length - 1,
})
}
const typeMatch = fieldContent.match(/type\s*:\s*['"](.*?)['"]/)
const description = extractDescription(fieldContent)
fieldPositions.forEach((field) => {
const startPos = field.start
let braceCount = 1
let endPos = startPos + 1
while (endPos < outputsContent.length && braceCount > 0) {
if (outputsContent[endPos] === '{') braceCount++
else if (outputsContent[endPos] === '}') braceCount--
endPos++
}
if (braceCount === 0) {
const fieldContent = outputsContent.substring(startPos + 1, endPos - 1).trim()
const typeMatch = fieldContent.match(/type\s*:\s*['"](.*?)['"]/)
const description = extractDescription(fieldContent)
if (typeMatch) {
outputs[field.name] = {
type: typeMatch[1],
description: description || `${field.name} output from the block`,
}
if (typeMatch) {
outputs[field.name] = {
type: typeMatch[1],
description: description || `${field.name} output from the block`,
}
}
})
if (Object.keys(outputs).length > 0) {
return outputs
}
}
})
return {}
return outputs
}
function extractToolsAccessFromContent(content: string): string[] {
@@ -1148,16 +1103,9 @@ function resolveConstReference(
// Extract the const content
const startIndex = constMatch.index + constMatch[0].length - 1
let braceCount = 1
let endIndex = startIndex + 1
const endIndex = findMatchingClose(typesContent, startIndex)
while (endIndex < typesContent.length && braceCount > 0) {
if (typesContent[endIndex] === '{') braceCount++
else if (typesContent[endIndex] === '}') braceCount--
endIndex++
}
if (braceCount !== 0) {
if (endIndex === -1) {
return null
}
@@ -1242,14 +1190,8 @@ function parseConstProperties(
if ((propName === 'properties' || propName === 'type') && !constRef) {
// Peek at what's inside the braces
const startPos = match.index + match[0].length - 1
let braceCount = 1
let endPos = startPos + 1
while (endPos < content.length && braceCount > 0) {
if (content[endPos] === '{') braceCount++
else if (content[endPos] === '}') braceCount--
endPos++
}
if (braceCount === 0) {
const endPos = findMatchingClose(content, startPos)
if (endPos !== -1) {
const propContent = content.substring(startPos + 1, endPos - 1).trim()
// If it starts with 'type:', it's an output field definition - process it
if (propContent.match(/^\s*type\s*:/)) {
@@ -1272,17 +1214,9 @@ function parseConstProperties(
} else {
// This property has inline definition
const startPos = match.index + match[0].length - 1
const endPos = findMatchingClose(content, startPos)
let braceCount = 1
let endPos = startPos + 1
while (endPos < content.length && braceCount > 0) {
if (content[endPos] === '{') braceCount++
else if (content[endPos] === '}') braceCount--
endPos++
}
if (braceCount === 0) {
if (endPos !== -1) {
const propContent = content.substring(startPos + 1, endPos - 1).trim()
const parsedProp = parseConstFieldContent(propContent, toolPrefix, typesContent, depth)
if (parsedProp) {
@@ -1324,16 +1258,9 @@ function resolveConstFromTypesContent(
}
const startIndex = constMatch.index + constMatch[0].length - 1
let braceCount = 1
let endIndex = startIndex + 1
const endIndex = findMatchingClose(typesContent, startIndex)
while (endIndex < typesContent.length && braceCount > 0) {
if (typesContent[endIndex] === '{') braceCount++
else if (typesContent[endIndex] === '}') braceCount--
endIndex++
}
if (braceCount !== 0) return null
if (endIndex === -1) return null
const constContent = typesContent.substring(startIndex + 1, endIndex - 1).trim()
@@ -1414,16 +1341,9 @@ function parseConstFieldContent(
const propertiesStart = fieldContent.search(/properties\s*:\s*\{/)
if (propertiesStart !== -1) {
const braceStart = fieldContent.indexOf('{', propertiesStart)
let braceCount = 1
let braceEnd = braceStart + 1
const braceEnd = findMatchingClose(fieldContent, braceStart)
while (braceEnd < fieldContent.length && braceCount > 0) {
if (fieldContent[braceEnd] === '{') braceCount++
else if (fieldContent[braceEnd] === '}') braceCount--
braceEnd++
}
if (braceCount === 0) {
if (braceEnd !== -1) {
const propertiesContent = fieldContent.substring(braceStart + 1, braceEnd - 1).trim()
result.properties = parseConstProperties(
propertiesContent,
@@ -1452,16 +1372,9 @@ function parseConstFieldContent(
const itemsStart = fieldContent.search(/items\s*:\s*\{/)
if (itemsStart !== -1) {
const braceStart = fieldContent.indexOf('{', itemsStart)
let braceCount = 1
let braceEnd = braceStart + 1
const braceEnd = findMatchingClose(fieldContent, braceStart)
while (braceEnd < fieldContent.length && braceCount > 0) {
if (fieldContent[braceEnd] === '{') braceCount++
else if (fieldContent[braceEnd] === '}') braceCount--
braceEnd++
}
if (braceCount === 0) {
if (braceEnd !== -1) {
const itemsContent = fieldContent.substring(braceStart + 1, braceEnd - 1).trim()
const itemsType = itemsContent.match(/type\s*:\s*['"]([^'"]+)['"]/)
const itemsDesc = extractDescription(itemsContent)
@@ -1516,6 +1429,40 @@ function parseConstFieldContent(
return result
}
/**
* Extract outputs from a tool content block by trying:
* 1. Const reference (e.g., `outputs: GIT_REF_OUTPUT_PROPERTIES,`)
* 2. Inline object (e.g., `outputs: { id: { type: 'string', ... } }`)
*/
function extractOutputsFromToolContent(
content: string,
toolPrefix: string
): Record<string, any> {
const constMatch = content.match(
/(?<![a-zA-Z_])outputs\s*:\s*([A-Z][A-Z_0-9]+)\s*(?:,|\}|$)/
)
if (constMatch) {
const resolved = resolveConstReference(constMatch[1], toolPrefix)
if (resolved && typeof resolved === 'object') {
return resolved
}
}
const outputsStart = content.search(/(?<![a-zA-Z_])outputs\s*:\s*{/)
if (outputsStart !== -1) {
const openBracePos = content.indexOf('{', outputsStart)
if (openBracePos !== -1) {
const closePos = findMatchingClose(content, openBracePos)
if (closePos !== -1) {
const outputsContent = content.substring(openBracePos + 1, closePos - 1).trim()
return parseToolOutputsField(outputsContent, toolPrefix)
}
}
}
return {}
}
function extractToolInfo(
toolName: string,
fileContent: string
@@ -1539,16 +1486,9 @@ function extractToolInfo(
if (exportMatch && exportMatch.index !== undefined) {
const startIndex = exportMatch.index + exportMatch[0].length - 1
let braceCount = 1
let endIndex = startIndex + 1
const endIndex = findMatchingClose(fileContent, startIndex)
while (endIndex < fileContent.length && braceCount > 0) {
if (fileContent[endIndex] === '{') braceCount++
else if (fileContent[endIndex] === '}') braceCount--
endIndex++
}
if (braceCount === 0) {
if (endIndex !== -1) {
toolContent = fileContent.substring(startIndex, endIndex)
}
}
@@ -1650,19 +1590,9 @@ function extractToolInfo(
continue
}
let braceCount = 1
let endPos = startPos + 1
const endPos = findMatchingClose(paramsContent, startPos)
while (endPos < paramsContent.length && braceCount > 0) {
if (paramsContent[endPos] === '{') {
braceCount++
} else if (paramsContent[endPos] === '}') {
braceCount--
}
endPos++
}
if (braceCount === 0) {
if (endPos !== -1) {
const paramBlock = paramsContent.substring(startPos + 1, endPos - 1).trim()
paramPositions.push({ name: paramName, start: startPos, content: paramBlock })
}
@@ -1704,36 +1634,43 @@ function extractToolInfo(
// Get the tool prefix for resolving const references
const toolPrefix = getToolPrefixFromName(toolName)
let outputs: Record<string, any> = {}
let outputs = extractOutputsFromToolContent(toolContent, toolPrefix)
// Pattern 1: outputs directly assigned to a const (e.g., "outputs: GIT_REF_OUTPUT_PROPERTIES,")
const directConstMatch = toolContent.match(
/(?<![a-zA-Z_])outputs\s*:\s*([A-Z][A-Z_0-9]+)\s*(?:,|\}|$)/
)
if (directConstMatch) {
const constName = directConstMatch[1]
const resolvedConst = resolveConstReference(constName, toolPrefix)
if (resolvedConst && typeof resolvedConst === 'object') {
outputs = resolvedConst
}
}
// Pattern 2: outputs is an object with properties (e.g., "outputs: { ... }")
// If no outputs found, check for spread inheritance (e.g., "...extendParserTool")
// toolContent may be narrowed past the spread line, so reconstruct the full block
if (Object.keys(outputs).length === 0) {
const outputsStart = toolContent.search(/(?<![a-zA-Z_])outputs\s*:\s*{/)
if (outputsStart !== -1) {
const openBracePos = toolContent.indexOf('{', outputsStart)
if (openBracePos !== -1) {
let braceCount = 1
let pos = openBracePos + 1
while (pos < toolContent.length && braceCount > 0) {
if (toolContent[pos] === '{') braceCount++
else if (toolContent[pos] === '}') braceCount--
pos++
let fullToolBlock = toolContent
if (toolIdMatch && toolIdMatch.index !== undefined) {
const beforeId = fileContent.substring(0, toolIdMatch.index)
const exportRegex = /export\s+const\s+\w+[^=]*=\s*\{/g
let lastExportMatch: RegExpExecArray | null = null
let m: RegExpExecArray | null = null
while ((m = exportRegex.exec(beforeId)) !== null) {
lastExportMatch = m
}
if (lastExportMatch && lastExportMatch.index !== undefined) {
const bracePos =
lastExportMatch.index + lastExportMatch[0].length - 1
const ep = findMatchingClose(fileContent, bracePos)
if (ep !== -1) {
fullToolBlock = fileContent.substring(bracePos, ep)
}
if (braceCount === 0) {
const outputsContent = toolContent.substring(openBracePos + 1, pos - 1).trim()
outputs = parseToolOutputsField(outputsContent, toolPrefix)
}
}
const spreadMatch = fullToolBlock.match(/\.\.\.(\w+(?:Tool|Base)\w*)/)
if (spreadMatch) {
const baseVarName = spreadMatch[1]
const baseToolRegex = new RegExp(
`export\\s+const\\s+${baseVarName}(?=[^a-zA-Z0-9_]|$)[^=]*=\\s*\\{`
)
const baseToolMatch = fileContent.match(baseToolRegex)
if (baseToolMatch && baseToolMatch.index !== undefined) {
const baseStart =
baseToolMatch.index + baseToolMatch[0].length - 1
const endIdx = findMatchingClose(fileContent, baseStart)
if (endIdx !== -1) {
const baseToolContent = fileContent.substring(baseStart, endIdx)
outputs = extractOutputsFromToolContent(baseToolContent, toolPrefix)
}
}
}
@@ -1921,24 +1858,15 @@ function parseToolOutputsField(outputsContent: string, toolPrefix?: string): Rec
const openBrace = braces.find((b) => b.type === 'open' && b.pos === bracePos)
if (openBrace) {
let braceCount = 1
let endPos = bracePos + 1
while (endPos < outputsContent.length && braceCount > 0) {
if (outputsContent[endPos] === '{') {
braceCount++
} else if (outputsContent[endPos] === '}') {
braceCount--
}
endPos++
const endPos = findMatchingClose(outputsContent, bracePos)
if (endPos !== -1) {
fieldPositions.push({
name: fieldName,
start: bracePos,
end: endPos,
level: openBrace.level,
})
}
fieldPositions.push({
name: fieldName,
start: bracePos,
end: endPos,
level: openBrace.level,
})
}
}
@@ -2001,16 +1929,9 @@ function parseFieldContent(fieldContent: string, toolPrefix?: string): any {
if (propertiesStart !== -1) {
const braceStart = fieldContent.indexOf('{', propertiesStart)
let braceCount = 1
let braceEnd = braceStart + 1
const braceEnd = findMatchingClose(fieldContent, braceStart)
while (braceEnd < fieldContent.length && braceCount > 0) {
if (fieldContent[braceEnd] === '{') braceCount++
else if (fieldContent[braceEnd] === '}') braceCount--
braceEnd++
}
if (braceCount === 0) {
if (braceEnd !== -1) {
const propertiesContent = fieldContent.substring(braceStart + 1, braceEnd - 1).trim()
result.properties = parsePropertiesContent(propertiesContent, toolPrefix)
}
@@ -2031,16 +1952,9 @@ function parseFieldContent(fieldContent: string, toolPrefix?: string): any {
if (itemsStart !== -1) {
const braceStart = fieldContent.indexOf('{', itemsStart)
let braceCount = 1
let braceEnd = braceStart + 1
const braceEnd = findMatchingClose(fieldContent, braceStart)
while (braceEnd < fieldContent.length && braceCount > 0) {
if (fieldContent[braceEnd] === '{') braceCount++
else if (fieldContent[braceEnd] === '}') braceCount--
braceEnd++
}
if (braceCount === 0) {
if (braceEnd !== -1) {
const itemsContent = fieldContent.substring(braceStart + 1, braceEnd - 1).trim()
const itemsType = itemsContent.match(/type\s*:\s*['"]([^'"]+)['"]/)
@@ -2213,19 +2127,9 @@ function parsePropertiesContent(
const startPos = match.index + match[0].length - 1
let braceCount = 1
let endPos = startPos + 1
const endPos = findMatchingClose(propertiesContent, startPos)
while (endPos < propertiesContent.length && braceCount > 0) {
if (propertiesContent[endPos] === '{') {
braceCount++
} else if (propertiesContent[endPos] === '}') {
braceCount--
}
endPos++
}
if (braceCount === 0) {
if (endPos !== -1) {
const propContent = propertiesContent.substring(startPos + 1, endPos - 1).trim()
const hasDescription = /description\s*:\s*/.test(propContent)
@@ -2729,16 +2633,9 @@ async function getHiddenAndVisibleBlockTypes(): Promise<{
const startIndex = match.index + match[0].length - 1
// Extract the block content
let braceCount = 1
let endIndex = startIndex + 1
const endIndex = findMatchingClose(fileContent, startIndex)
while (endIndex < fileContent.length && braceCount > 0) {
if (fileContent[endIndex] === '{') braceCount++
else if (fileContent[endIndex] === '}') braceCount--
endIndex++
}
if (braceCount === 0) {
if (endIndex !== -1) {
const blockContent = fileContent.substring(startIndex, endIndex)
const blockType = extractStringPropertyFromContent(blockContent, 'type', true)