#!/usr/bin/env ts-node import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import { glob } from 'glob' console.log('Starting documentation generator...') // Define directory paths const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const rootDir = path.resolve(__dirname, '..') // Paths configuration const BLOCKS_PATH = path.join(rootDir, 'apps/sim/blocks/blocks') const DOCS_OUTPUT_PATH = path.join(rootDir, 'apps/docs/content/docs/en/tools') const ICONS_PATH = path.join(rootDir, 'apps/sim/components/icons.tsx') // Make sure the output directory exists if (!fs.existsSync(DOCS_OUTPUT_PATH)) { fs.mkdirSync(DOCS_OUTPUT_PATH, { recursive: true }) } // Basic interface for BlockConfig to avoid import issues interface BlockConfig { type: string name: string description: string longDescription?: string category: string bgColor?: string outputs?: Record tools?: { access?: string[] } [key: string]: any } // Function to extract SVG icons from icons.tsx file function extractIcons(): Record { try { const iconsContent = fs.readFileSync(ICONS_PATH, 'utf-8') const icons: Record = {} // Match both function declaration and arrow function export patterns const functionDeclarationRegex = /export\s+function\s+(\w+Icon)\s*\([^)]*\)\s*{[\s\S]*?return\s*\(\s*\s*\)/g const arrowFunctionRegex = /export\s+const\s+(\w+Icon)\s*=\s*\([^)]*\)\s*=>\s*(\(?\s*\s*\)?)/g // Extract function declaration style icons const functionMatches = Array.from(iconsContent.matchAll(functionDeclarationRegex)) for (const match of functionMatches) { const iconName = match[1] const svgMatch = match[0].match(//) if (iconName && svgMatch) { // Clean the SVG to remove {...props} and standardize size let svgContent = svgMatch[0] svgContent = svgContent.replace(/{\.\.\.props}/g, '') svgContent = svgContent.replace(/{\.\.\.(props|rest)}/g, '') // Remove any existing width/height attributes to let CSS handle sizing svgContent = svgContent.replace(/width=["'][^"']*["']/g, '') svgContent = svgContent.replace(/height=["'][^"']*["']/g, '') // Add className for styling svgContent = svgContent.replace(//) if (iconName && svgMatch) { // Clean the SVG to remove {...props} and standardize size let cleanedSvg = svgMatch[0] cleanedSvg = cleanedSvg.replace(/{\.\.\.props}/g, '') cleanedSvg = cleanedSvg.replace(/{\.\.\.(props|rest)}/g, '') // Remove any existing width/height attributes to let CSS handle sizing cleanedSvg = cleanedSvg.replace(/width=["'][^"']*["']/g, '') cleanedSvg = cleanedSvg.replace(/height=["'][^"']*["']/g, '') // Add className for styling cleanedSvg = cleanedSvg.replace(/ 0) { if (afterExport[blockEnd] === '{') braceCount++ else if (afterExport[blockEnd] === '}') braceCount-- blockEnd++ } // Extract the block content and look for type const blockContent = afterExport.substring(blockStart, blockEnd) const typeMatch = blockContent.match(/type\s*:\s*['"]([^'"]+)['"]/) if (typeMatch) return typeMatch[1] } } // Convert CamelCase to snake_case as fallback return blockName .replace(/([A-Z])/g, '_$1') .toLowerCase() .replace(/^_/, '') } // Helper to extract a string property from content function extractStringProperty(content: string, propName: string): string | null { // Try single quotes first - more permissive approach const singleQuoteMatch = content.match(new RegExp(`${propName}\\s*:\\s*'(.*?)'`, 'm')) if (singleQuoteMatch) return singleQuoteMatch[1] // Try double quotes const doubleQuoteMatch = content.match(new RegExp(`${propName}\\s*:\\s*"(.*?)"`, 'm')) if (doubleQuoteMatch) return doubleQuoteMatch[1] // Try to match multi-line string with template literals const templateMatch = content.match(new RegExp(`${propName}\\s*:\\s*\`([^\`]+)\``, 's')) if (templateMatch) { let templateContent = templateMatch[1] // Handle template literals with expressions by replacing them with reasonable defaults // This is a simple approach - we'll replace common variable references with sensible defaults templateContent = templateContent.replace( /\$\{[^}]*shouldEnableURLInput[^}]*\?[^:]*:[^}]*\}/g, 'Upload files directly. ' ) templateContent = templateContent.replace(/\$\{[^}]*shouldEnableURLInput[^}]*\}/g, 'false') // Remove any remaining template expressions that we can't safely evaluate templateContent = templateContent.replace(/\$\{[^}]+\}/g, '') // Clean up any extra whitespace templateContent = templateContent.replace(/\s+/g, ' ').trim() return templateContent } return null } // Helper to extract icon name from content function extractIconName(content: string): string | null { const iconMatch = content.match(/icon\s*:\s*(\w+Icon)/) return iconMatch ? iconMatch[1] : null } // Updated function to extract outputs with a simpler and more reliable approach function extractOutputs(content: string): Record { // Look for the outputs section using balanced brace matching const outputsStart = content.search(/outputs\s*:\s*{/) if (outputsStart === -1) return {} // Find the opening brace position const openBracePos = content.indexOf('{', outputsStart) if (openBracePos === -1) return {} // Use balanced brace counting to find the complete outputs section let braceCount = 1 let pos = openBracePos + 1 while (pos < content.length && braceCount > 0) { if (content[pos] === '{') { braceCount++ } else if (content[pos] === '}') { braceCount-- } pos++ } if (braceCount === 0) { const outputsContent = content.substring(openBracePos + 1, pos - 1).trim() const outputs: Record = {} // First try to handle the new object format: fieldName: { type: 'type', description: 'desc' } // Use a more robust approach to extract field definitions const fieldRegex = /(\w+)\s*:\s*{/g let match const fieldPositions: Array<{ name: string; start: number }> = [] // Find all field starting positions while ((match = fieldRegex.exec(outputsContent)) !== null) { fieldPositions.push({ name: match[1], start: match.index + match[0].length - 1, // Position of the opening brace }) } // Extract each field's content by finding balanced braces fieldPositions.forEach((field) => { const startPos = field.start let braceCount = 1 let endPos = startPos + 1 // Find the matching closing brace while (endPos < outputsContent.length && braceCount > 0) { if (outputsContent[endPos] === '{') { braceCount++ } else if (outputsContent[endPos] === '}') { braceCount-- } endPos++ } if (braceCount === 0) { // Extract the content between braces const fieldContent = outputsContent.substring(startPos + 1, endPos - 1).trim() // Extract type and description from the object const typeMatch = fieldContent.match(/type\s*:\s*['"](.*?)['"]/) const descriptionMatch = fieldContent.match(/description\s*:\s*['"](.*?)['"]/) if (typeMatch) { outputs[field.name] = { type: typeMatch[1], description: descriptionMatch ? descriptionMatch[1] : `${field.name} output from the block`, } } } }) // If we found object fields, return them if (Object.keys(outputs).length > 0) { return outputs } // Fallback: try to handle the old flat format: fieldName: 'type' const flatFieldMatches = outputsContent.match(/(\w+)\s*:\s*['"](.*?)['"]/g) if (flatFieldMatches && flatFieldMatches.length > 0) { flatFieldMatches.forEach((fieldMatch) => { const fieldParts = fieldMatch.match(/(\w+)\s*:\s*['"](.*?)['"]/) if (fieldParts) { const fieldName = fieldParts[1] const fieldType = fieldParts[2] outputs[fieldName] = { type: fieldType, description: `${fieldName} output from the block`, } } }) // If we found flat fields, return them if (Object.keys(outputs).length > 0) { return outputs } } } return {} } // Helper to extract tools access array function extractToolsAccess(content: string): string[] { const accessMatch = content.match(/access\s*:\s*\[\s*((?:['"][^'"]+['"](?:\s*,\s*)?)+)\s*\]/) if (!accessMatch) return [] const accessContent = accessMatch[1] const tools: string[] = [] const toolMatches = accessContent.match(/['"]([^'"]+)['"]/g) if (toolMatches) { toolMatches.forEach((toolText) => { const match = toolText.match(/['"]([^'"]+)['"]/) if (match) { tools.push(match[1]) } }) } return tools } // Function to extract tool information from file content function extractToolInfo( toolName: string, fileContent: string ): { description: string params: Array<{ name: string; type: string; required: boolean; description: string }> outputs: Record } | null { try { // Extract tool config section - Match params until the next top-level property const toolConfigRegex = /params\s*:\s*{([\s\S]*?)},?\s*(?:outputs|oauth|request|directExecution|postProcess|transformResponse)/ const toolConfigMatch = fileContent.match(toolConfigRegex) // Extract description const descriptionRegex = /description\s*:\s*['"](.*?)['"].*/ const descriptionMatch = fileContent.match(descriptionRegex) const description = descriptionMatch ? descriptionMatch[1] : 'No description available' // Parse parameters const params: Array<{ name: string; type: string; required: boolean; description: string }> = [] if (toolConfigMatch) { const paramsContent = toolConfigMatch[1] // More robust approach to extract parameters with balanced brace matching // Extract each parameter block completely const paramBlocksRegex = /(\w+)\s*:\s*{/g let paramMatch const paramPositions: Array<{ name: string; start: number; content: string }> = [] while ((paramMatch = paramBlocksRegex.exec(paramsContent)) !== null) { const paramName = paramMatch[1] const startPos = paramMatch.index + paramMatch[0].length - 1 // Position of opening brace // Find matching closing brace using balanced counting let braceCount = 1 let endPos = startPos + 1 while (endPos < paramsContent.length && braceCount > 0) { if (paramsContent[endPos] === '{') { braceCount++ } else if (paramsContent[endPos] === '}') { braceCount-- } endPos++ } if (braceCount === 0) { const paramBlock = paramsContent.substring(startPos + 1, endPos - 1).trim() paramPositions.push({ name: paramName, start: startPos, content: paramBlock }) } } for (const param of paramPositions) { const paramName = param.name const paramBlock = param.content // Skip the accessToken parameter as it's handled automatically by the OAuth flow // Also skip any params parameter which isn't a real input if (paramName === 'accessToken' || paramName === 'params' || paramName === 'tools') { continue } // Extract param details with more robust patterns const typeMatch = paramBlock.match(/type\s*:\s*['"]([^'"]+)['"]/) const requiredMatch = paramBlock.match(/required\s*:\s*(true|false)/) // More careful extraction of description with handling for multiline descriptions let descriptionMatch = paramBlock.match(/description\s*:\s*'(.*?)'(?=\s*[,}])/s) if (!descriptionMatch) { descriptionMatch = paramBlock.match(/description\s*:\s*"(.*?)"(?=\s*[,}])/s) } if (!descriptionMatch) { // Try for template literals if the description uses backticks descriptionMatch = paramBlock.match(/description\s*:\s*`([^`]+)`/s) } if (!descriptionMatch) { // Handle multi-line descriptions without ending quote on same line descriptionMatch = paramBlock.match( /description\s*:\s*['"]([^'"]*(?:\n[^'"]*)*?)['"](?=\s*[,}])/s ) } params.push({ name: paramName, type: typeMatch ? typeMatch[1] : 'string', required: requiredMatch ? requiredMatch[1] === 'true' : false, description: descriptionMatch ? descriptionMatch[1] : 'No description', }) } } // First priority: Extract outputs from the new outputs field in ToolConfig let outputs: Record = {} const outputsFieldRegex = /outputs\s*:\s*{([\s\S]*?)}\s*,?\s*(?:oauth|params|request|directExecution|postProcess|transformResponse|$|\})/ const outputsFieldMatch = fileContent.match(outputsFieldRegex) if (outputsFieldMatch) { const outputsContent = outputsFieldMatch[1] outputs = parseToolOutputsField(outputsContent) console.log(`Found tool outputs field for ${toolName}:`, Object.keys(outputs)) } return { description, params, outputs, } } catch (error) { console.error(`Error extracting info for tool ${toolName}:`, error) return null } } // Helper function to recursively format output structure for documentation function formatOutputStructure(outputs: Record, indentLevel = 0): string { let result = '' for (const [key, output] of Object.entries(outputs)) { let type = 'unknown' let description = `${key} output from the tool` if (typeof output === 'object' && output !== null) { if (output.type) { type = output.type } if (output.description) { description = output.description } } // Escape special characters in the description const escapedDescription = description .replace(/\|/g, '\\|') .replace(/\{/g, '\\{') .replace(/\}/g, '\\}') .replace(/\(/g, '\\(') .replace(/\)/g, '\\)') .replace(/\[/g, '\\[') .replace(/\]/g, '\\]') .replace(//g, '>') // Create prefix based on nesting level with visual hierarchy let prefix = '' if (indentLevel === 1) { prefix = '↳ ' } else if (indentLevel >= 2) { // For deeper nesting (like array items), use indented arrows prefix = ' ↳ ' } // For arrays, expand nested items if (typeof output === 'object' && output !== null && output.type === 'array') { result += `| ${prefix}\`${key}\` | ${type} | ${escapedDescription} |\n` // Handle array items with properties (nested TWO more levels to show it's inside the array) if (output.items?.properties) { // Create a visual separator to show these are array item properties const arrayItemsResult = formatOutputStructure(output.items.properties, indentLevel + 2) result += arrayItemsResult } } // For objects, expand properties else if ( typeof output === 'object' && output !== null && output.properties && (output.type === 'object' || output.type === 'json') ) { result += `| ${prefix}\`${key}\` | ${type} | ${escapedDescription} |\n` const nestedResult = formatOutputStructure(output.properties, indentLevel + 1) result += nestedResult } // For simple types, show with prefix if nested else { result += `| ${prefix}\`${key}\` | ${type} | ${escapedDescription} |\n` } } return result } // New function to parse the structured outputs field from ToolConfig function parseToolOutputsField(outputsContent: string): Record { const outputs: Record = {} // Calculate nesting levels for all braces first const braces: Array<{ type: 'open' | 'close'; pos: number; level: number }> = [] for (let i = 0; i < outputsContent.length; i++) { if (outputsContent[i] === '{') { braces.push({ type: 'open', pos: i, level: 0 }) } else if (outputsContent[i] === '}') { braces.push({ type: 'close', pos: i, level: 0 }) } } // Calculate actual nesting levels let currentLevel = 0 for (const brace of braces) { if (brace.type === 'open') { brace.level = currentLevel currentLevel++ } else { currentLevel-- brace.level = currentLevel } } // Find field definitions and their nesting levels const fieldStartRegex = /(\w+)\s*:\s*{/g let match const fieldPositions: Array<{ name: string; start: number; end: number; level: number }> = [] while ((match = fieldStartRegex.exec(outputsContent)) !== null) { const fieldName = match[1] const bracePos = match.index + match[0].length - 1 // Find the corresponding opening brace to determine nesting level const openBrace = braces.find((b) => b.type === 'open' && b.pos === bracePos) if (openBrace) { // Find the matching closing brace let braceCount = 1 let endPos = bracePos + 1 while (endPos < outputsContent.length && braceCount > 0) { if (outputsContent[endPos] === '{') { braceCount++ } else if (outputsContent[endPos] === '}') { braceCount-- } endPos++ } fieldPositions.push({ name: fieldName, start: bracePos, end: endPos, level: openBrace.level, }) } } // Only process level 0 fields (top-level outputs) const topLevelFields = fieldPositions.filter((f) => f.level === 0) topLevelFields.forEach((field) => { const fieldContent = outputsContent.substring(field.start + 1, field.end - 1).trim() // Parse the field content const parsedField = parseFieldContent(fieldContent) if (parsedField) { outputs[field.name] = parsedField } }) return outputs } // Helper function to parse individual field content with support for nested structures function parseFieldContent(fieldContent: string): any { // Extract type and description const typeMatch = fieldContent.match(/type\s*:\s*['"]([^'"]+)['"]/) const descMatch = fieldContent.match(/description\s*:\s*['"`]([^'"`\n]+)['"`]/) if (!typeMatch) return null const fieldType = typeMatch[1] const description = descMatch ? descMatch[1] : '' const result: any = { type: fieldType, description: description, } // Check for properties (nested objects) - only for object types, not arrays if (fieldType === 'object' || fieldType === 'json') { const propertiesRegex = /properties\s*:\s*{/ const propertiesStart = fieldContent.search(propertiesRegex) if (propertiesStart !== -1) { const braceStart = fieldContent.indexOf('{', propertiesStart) let braceCount = 1 let braceEnd = braceStart + 1 // Find matching closing brace while (braceEnd < fieldContent.length && braceCount > 0) { if (fieldContent[braceEnd] === '{') braceCount++ else if (fieldContent[braceEnd] === '}') braceCount-- braceEnd++ } if (braceCount === 0) { const propertiesContent = fieldContent.substring(braceStart + 1, braceEnd - 1).trim() result.properties = parsePropertiesContent(propertiesContent) } } } // Check for items (array items) - ensure balanced brace matching const itemsRegex = /items\s*:\s*{/ const itemsStart = fieldContent.search(itemsRegex) if (itemsStart !== -1) { const braceStart = fieldContent.indexOf('{', itemsStart) let braceCount = 1 let braceEnd = braceStart + 1 // Find matching closing brace while (braceEnd < fieldContent.length && braceCount > 0) { if (fieldContent[braceEnd] === '{') braceCount++ else if (fieldContent[braceEnd] === '}') braceCount-- braceEnd++ } if (braceCount === 0) { const itemsContent = fieldContent.substring(braceStart + 1, braceEnd - 1).trim() const itemsType = itemsContent.match(/type\s*:\s*['"]([^'"]+)['"]/) // Only look for description before any properties block to avoid picking up nested property descriptions const propertiesStart = itemsContent.search(/properties\s*:\s*{/) const searchContent = propertiesStart >= 0 ? itemsContent.substring(0, propertiesStart) : itemsContent const itemsDesc = searchContent.match(/description\s*:\s*['"`]([^'"`\n]+)['"`]/) result.items = { type: itemsType ? itemsType[1] : 'object', description: itemsDesc ? itemsDesc[1] : '', } // Check if items have properties const itemsPropertiesRegex = /properties\s*:\s*{/ const itemsPropsStart = itemsContent.search(itemsPropertiesRegex) if (itemsPropsStart !== -1) { const propsBraceStart = itemsContent.indexOf('{', itemsPropsStart) let propsBraceCount = 1 let propsBraceEnd = propsBraceStart + 1 while (propsBraceEnd < itemsContent.length && propsBraceCount > 0) { if (itemsContent[propsBraceEnd] === '{') propsBraceCount++ else if (itemsContent[propsBraceEnd] === '}') propsBraceCount-- propsBraceEnd++ } if (propsBraceCount === 0) { const itemsPropsContent = itemsContent .substring(propsBraceStart + 1, propsBraceEnd - 1) .trim() result.items.properties = parsePropertiesContent(itemsPropsContent) } } } } return result } // Helper function to parse properties content recursively function parsePropertiesContent(propertiesContent: string): Record { const properties: Record = {} // Find property definitions using balanced brace matching, but exclude type-only definitions const propStartRegex = /(\w+)\s*:\s*{/g let match const propPositions: Array<{ name: string; start: number; content: string }> = [] while ((match = propStartRegex.exec(propertiesContent)) !== null) { const propName = match[1] // Skip structural keywords that should never be treated as property names if (propName === 'items' || propName === 'properties') { continue } const startPos = match.index + match[0].length - 1 // Position of opening brace // Find the matching closing brace let braceCount = 1 let endPos = startPos + 1 while (endPos < propertiesContent.length && braceCount > 0) { if (propertiesContent[endPos] === '{') { braceCount++ } else if (propertiesContent[endPos] === '}') { braceCount-- } endPos++ } if (braceCount === 0) { const propContent = propertiesContent.substring(startPos + 1, endPos - 1).trim() // Skip if this is just a type definition (contains only 'type' field) rather than a real property // This happens with array items definitions like: items: { type: 'string' } // More precise check: only skip if it ONLY has 'type' and nothing else meaningful const hasDescription = /description\s*:\s*/.test(propContent) const hasProperties = /properties\s*:\s*{/.test(propContent) const hasItems = /items\s*:\s*{/.test(propContent) const isTypeOnly = !hasDescription && !hasProperties && !hasItems && /^type\s*:\s*['"].*?['"]\s*,?\s*$/.test(propContent) if (!isTypeOnly) { propPositions.push({ name: propName, start: startPos, content: propContent, }) } } } // Process the actual property definitions propPositions.forEach((prop) => { const parsedProp = parseFieldContent(prop.content) if (parsedProp) { properties[prop.name] = parsedProp } }) return properties } // Find and extract information about a tool async function getToolInfo(toolName: string): Promise<{ description: string params: Array<{ name: string; type: string; required: boolean; description: string }> outputs: Record } | null> { try { // Split the tool name into parts const parts = toolName.split('_') // Try to find the correct split point by checking if directories exist let toolPrefix = '' let toolSuffix = '' // Start from the longest possible prefix and work backwards for (let i = parts.length - 1; i >= 1; i--) { const possiblePrefix = parts.slice(0, i).join('_') const possibleSuffix = parts.slice(i).join('_') // Check if a directory exists for this prefix const toolDirPath = path.join(rootDir, `apps/sim/tools/${possiblePrefix}`) if (fs.existsSync(toolDirPath) && fs.statSync(toolDirPath).isDirectory()) { toolPrefix = possiblePrefix toolSuffix = possibleSuffix break } } // If no directory was found, fall back to single-part prefix if (!toolPrefix) { toolPrefix = parts[0] toolSuffix = parts.slice(1).join('_') } // Simplify the file search strategy const possibleLocations = [] // Most common pattern: suffix.ts file in the prefix directory possibleLocations.push(path.join(rootDir, `apps/sim/tools/${toolPrefix}/${toolSuffix}.ts`)) // Try camelCase version of suffix const camelCaseSuffix = toolSuffix .split('_') .map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))) .join('') possibleLocations.push(path.join(rootDir, `apps/sim/tools/${toolPrefix}/${camelCaseSuffix}.ts`)) // Also check the index.ts file in the tool directory possibleLocations.push(path.join(rootDir, `apps/sim/tools/${toolPrefix}/index.ts`)) // Try to find the tool definition file let toolFileContent = '' for (const location of possibleLocations) { if (fs.existsSync(location)) { toolFileContent = fs.readFileSync(location, 'utf-8') break } } if (!toolFileContent) { console.warn(`Could not find definition for tool: ${toolName}`) return null } // Extract tool information from the file return extractToolInfo(toolName, toolFileContent) } catch (error) { console.error(`Error getting info for tool ${toolName}:`, error) return null } } // Function to extract content between manual content markers function extractManualContent(existingContent: string): Record { const manualSections: Record = {} // Improved regex to better handle MDX comments const manualContentRegex = /\{\/\*\s*MANUAL-CONTENT-START:(\w+)\s*\*\/\}([\s\S]*?)\{\/\*\s*MANUAL-CONTENT-END\s*\*\/\}/g let match while ((match = manualContentRegex.exec(existingContent)) !== null) { const sectionName = match[1] const content = match[2].trim() manualSections[sectionName] = content console.log(`Found manual content for section: ${sectionName}`) } return manualSections } // Function to merge generated markdown with manual content function mergeWithManualContent( generatedMarkdown: string, existingContent: string | null, manualSections: Record ): string { if (!existingContent || Object.keys(manualSections).length === 0) { return generatedMarkdown } console.log('Merging manual content with generated markdown') // Log what we found for debugging console.log(`Found ${Object.keys(manualSections).length} manual sections`) Object.keys(manualSections).forEach((section) => { console.log(` - ${section}: ${manualSections[section].substring(0, 20)}...`) }) // Replace placeholders in generated markdown with manual content let mergedContent = generatedMarkdown // Add manual content for each section we found Object.entries(manualSections).forEach(([sectionName, content]) => { // Define insertion points for different section types with improved patterns const insertionPoints: Record = { intro: { regex: /`}\s*\/>/, }, usage: { regex: /## Usage Instructions/, }, outputs: { regex: /## Outputs/, }, notes: { regex: /## Notes/, }, } // Find the appropriate insertion point const insertionPoint = insertionPoints[sectionName] if (insertionPoint) { // Use regex to find the insertion point const match = mergedContent.match(insertionPoint.regex) if (match && match.index !== undefined) { // Insert after the matched content const insertPosition = match.index + match[0].length console.log(`Inserting ${sectionName} content after position ${insertPosition}`) mergedContent = `${mergedContent.slice(0, insertPosition)}\n\n{/* MANUAL-CONTENT-START:${sectionName} */}\n${content}\n{/* MANUAL-CONTENT-END */}\n${mergedContent.slice(insertPosition)}` } else { console.log( `Could not find insertion point for ${sectionName}, regex pattern: ${insertionPoint.regex}` ) } } else { console.log(`No insertion point defined for section ${sectionName}`) } }) return mergedContent } // Function to generate documentation for a block async function generateBlockDoc(blockPath: string, icons: Record) { try { // Extract the block name from the file path const blockFileName = path.basename(blockPath, '.ts') if (blockFileName.endsWith('.test')) { return // Skip test files } // Read the file content const fileContent = fs.readFileSync(blockPath, 'utf-8') // Extract block configuration from the file content const blockConfig = extractBlockConfig(fileContent) if (!blockConfig || !blockConfig.type) { console.warn(`Skipping ${blockFileName} - not a valid block config`) return } // Skip blocks with category 'blocks' (except memory type), and skip specific blocks if ( (blockConfig.category === 'blocks' && blockConfig.type !== 'memory' && blockConfig.type !== 'knowledge') || blockConfig.type === 'evaluator' || blockConfig.type === 'number' ) { return } // Output file path const outputFilePath = path.join(DOCS_OUTPUT_PATH, `${blockConfig.type}.mdx`) // IMPORTANT: Check if file already exists and read its content FIRST let existingContent: string | null = null if (fs.existsSync(outputFilePath)) { existingContent = fs.readFileSync(outputFilePath, 'utf-8') console.log(`Existing file found for ${blockConfig.type}.mdx, checking for manual content...`) } // Extract manual content from existing file before generating new content const manualSections = existingContent ? extractManualContent(existingContent) : {} // Create the markdown content - now async const markdown = await generateMarkdownForBlock(blockConfig, icons) // Merge with manual content if we found any let finalContent = markdown if (Object.keys(manualSections).length > 0) { console.log(`Found manual content in ${blockConfig.type}.mdx, merging...`) finalContent = mergeWithManualContent(markdown, existingContent, manualSections) } else { console.log(`No manual content found in ${blockConfig.type}.mdx`) } // Write the markdown file fs.writeFileSync(outputFilePath, finalContent) console.log(`Generated documentation for ${blockConfig.type}`) } catch (error) { console.error(`Error processing ${blockPath}:`, error) } } // Update generateMarkdownForBlock to remove placeholders async function generateMarkdownForBlock( blockConfig: BlockConfig, icons: Record ): Promise { const { type, name, description, longDescription, category, bgColor, iconName, outputs = {}, tools = { access: [] }, } = blockConfig // Get SVG icon if available const iconSvg = iconName && icons[iconName] ? icons[iconName] : null // Generate the outputs section let outputsSection = '' if (outputs && Object.keys(outputs).length > 0) { outputsSection = '## Outputs\n\n' // Create the base outputs table outputsSection += '| Output | Type | Description |\n' outputsSection += '| ------ | ---- | ----------- |\n' // Process each output field for (const outputKey in outputs) { const output = outputs[outputKey] // Escape special characters in the description that could break markdown tables const escapedDescription = output.description ? output.description .replace(/\|/g, '\\|') // Escape pipe characters .replace(/\{/g, '\\{') // Escape curly braces .replace(/\}/g, '\\}') // Escape curly braces .replace(/\(/g, '\\(') // Escape opening parentheses .replace(/\)/g, '\\)') // Escape closing parentheses .replace(/\[/g, '\\[') // Escape opening brackets .replace(/\]/g, '\\]') // Escape closing brackets .replace(//g, '>') // Convert greater than to HTML entity : `Output from ${outputKey}` if (typeof output.type === 'string') { // Simple output with explicit type outputsSection += `| \`${outputKey}\` | ${output.type} | ${escapedDescription} |\n` } else if (output.type && typeof output.type === 'object') { // For cases where output.type is an object containing field types outputsSection += `| \`${outputKey}\` | object | ${escapedDescription} |\n` // Add properties directly to the main table with indentation for (const propName in output.type) { const propType = output.type[propName] // Get description from comments if available const commentMatch = propName && output.type[propName]._comment ? output.type[propName]._comment : `${propName} of the ${outputKey}` outputsSection += `| ↳ \`${propName}\` | ${propType} | ${commentMatch} |\n` } } else if (output.properties) { // Complex output with properties outputsSection += `| \`${outputKey}\` | object | ${escapedDescription} |\n` // Add properties directly to the main table with indentation for (const propName in output.properties) { const prop = output.properties[propName] // Escape special characters in the description const escapedPropertyDescription = prop.description ? prop.description .replace(/\|/g, '\\|') // Escape pipe characters .replace(/\{/g, '\\{') // Escape curly braces .replace(/\}/g, '\\}') // Escape curly braces .replace(/\(/g, '\\(') // Escape opening parentheses .replace(/\)/g, '\\)') // Escape closing parentheses .replace(/\[/g, '\\[') // Escape opening brackets .replace(/\]/g, '\\]') // Escape closing brackets .replace(//g, '>') // Convert greater than to HTML entity : `The ${propName} of the ${outputKey}` outputsSection += `| ↳ \`${propName}\` | ${prop.type} | ${escapedPropertyDescription} |\n` } } } } else { outputsSection = 'This block does not produce any outputs.' } // Create tools section with more details let toolsSection = '' if (tools.access?.length) { toolsSection = '## Tools\n\n' // For each tool, try to find its definition and extract parameter information for (const tool of tools.access) { toolsSection += `### \`${tool}\`\n\n` // Get dynamic tool information const toolInfo = await getToolInfo(tool) if (toolInfo) { if (toolInfo.description && toolInfo.description !== 'No description available') { toolsSection += `${toolInfo.description}\n\n` } // Add Input Parameters section for the tool toolsSection += '#### Input\n\n' toolsSection += '| Parameter | Type | Required | Description |\n' toolsSection += '| --------- | ---- | -------- | ----------- |\n' if (toolInfo.params.length > 0) { // Use dynamically extracted parameters for (const param of toolInfo.params) { // Escape special characters in the description that could break markdown tables const escapedDescription = param.description ? param.description .replace(/\|/g, '\\|') // Escape pipe characters .replace(/\{/g, '\\{') // Escape curly braces .replace(/\}/g, '\\}') // Escape curly braces .replace(/\(/g, '\\(') // Escape opening parentheses .replace(/\)/g, '\\)') // Escape closing parentheses .replace(/\[/g, '\\[') // Escape opening brackets .replace(/\]/g, '\\]') // Escape closing brackets .replace(//g, '>') // Convert greater than to HTML entity : 'No description' toolsSection += `| \`${param.name}\` | ${param.type} | ${param.required ? 'Yes' : 'No'} | ${escapedDescription} |\n` } } // Add Output Parameters section for the tool toolsSection += '\n#### Output\n\n' // Always prefer tool-specific outputs over block outputs for accuracy if (Object.keys(toolInfo.outputs).length > 0) { // Use tool-specific outputs (most accurate) toolsSection += '| Parameter | Type | Description |\n' toolsSection += '| --------- | ---- | ----------- |\n' // Use the enhanced formatOutputStructure function to handle nested structures toolsSection += formatOutputStructure(toolInfo.outputs) } else if (Object.keys(outputs).length > 0) { // Fallback to block outputs only if no tool outputs are available toolsSection += '| Parameter | Type | Description |\n' toolsSection += '| --------- | ---- | ----------- |\n' for (const [key, output] of Object.entries(outputs)) { let type = 'string' let description = `${key} output from the tool` if (typeof output === 'string') { type = output } else if (typeof output === 'object' && output !== null) { if ('type' in output && typeof output.type === 'string') { type = output.type } if ('description' in output && typeof output.description === 'string') { description = output.description } } // Escape special characters in the description const escapedDescription = description .replace(/\|/g, '\\|') .replace(/\{/g, '\\{') .replace(/\}/g, '\\}') .replace(/\(/g, '\\(') .replace(/\)/g, '\\)') .replace(/\[/g, '\\[') .replace(/\]/g, '\\]') .replace(//g, '>') toolsSection += `| \`${key}\` | ${type} | ${escapedDescription} |\n` } } else { toolsSection += 'This tool does not produce any outputs.\n' } } toolsSection += '\n' } } // Add usage instructions if available in block config let usageInstructions = '' if (longDescription) { usageInstructions = `## Usage Instructions\n\n${longDescription}\n\n` } // Generate the markdown content without any placeholders return `--- title: ${name} description: ${description} --- import { BlockInfoCard } from "@/components/ui/block-info-card" ${usageInstructions} ${toolsSection} ## Notes - Category: \`${category}\` - Type: \`${type}\` ` } // Main function to generate all block docs async function generateAllBlockDocs() { try { // Extract icons first const icons = extractIcons() // Get all block files const blockFiles = await glob(`${BLOCKS_PATH}/*.ts`) // Generate docs for each block for (const blockFile of blockFiles) { await generateBlockDoc(blockFile, icons) } // Update the meta.json file updateMetaJson() return true } catch (error) { console.error('Error generating documentation:', error) return false } } // Function to update the meta.json file with all blocks function updateMetaJson() { const metaJsonPath = path.join(DOCS_OUTPUT_PATH, 'meta.json') // Get all MDX files in the tools directory const blockFiles = fs .readdirSync(DOCS_OUTPUT_PATH) .filter((file: string) => file.endsWith('.mdx')) .map((file: string) => path.basename(file, '.mdx')) // Create meta.json structure // Keep "index" as the first item if it exists const items = [ ...(blockFiles.includes('index') ? ['index'] : []), ...blockFiles.filter((file: string) => file !== 'index').sort(), ] const metaJson = { items, } // Write the meta.json file fs.writeFileSync(metaJsonPath, JSON.stringify(metaJson, null, 2)) } // Run the script generateAllBlockDocs() .then((success) => { if (success) { console.log('Documentation generation completed successfully') process.exit(0) } else { console.error('Documentation generation failed') process.exit(1) } }) .catch((error) => { console.error('Fatal error:', error) process.exit(1) })