UNPKG

touchdesigner-mcp-server

Version:
316 lines (315 loc) 10 kB
/** * Module Help Formatter * * Formats TouchDesigner module help information with token optimization. * Used by GET_MODULE_HELP tool. */ import { DEFAULT_PRESENTER_FORMAT, presentStructuredData, } from "./presenter.js"; import { finalizeFormattedText, mergeFormatterOptions, } from "./responseFormatter.js"; /** * Format module help result */ export function formatModuleHelp(data, options) { const opts = mergeFormatterOptions(options); if (!data?.helpText) { return "No help information available."; } const moduleName = data.moduleName ?? "Unknown"; const helpText = data.helpText; const members = extractModuleMembers(helpText); const classInfo = extractClassSummary(helpText); if (opts.detailLevel === "detailed") { return formatDetailed(moduleName, helpText, opts.responseFormat); } let formattedText = ""; let context; switch (opts.detailLevel) { case "minimal": case "summary": { const summary = formatSummary(moduleName, helpText, members, classInfo); formattedText = summary.text; context = summary.context; break; } default: formattedText = helpText; context = buildHelpContext(moduleName, helpText, members, classInfo); } const ctx = context; return finalizeFormattedText(formattedText, opts, { context: ctx, structured: ctx, template: "moduleHelp", }); } /** * Summary mode: Module name with key sections */ function formatSummary(moduleName, helpText, members, classInfo) { const sections = extractHelpSections(helpText); const preview = extractHelpPreview(helpText, 500); const memberSummary = formatMemberSummary(members); const lines = [`✓ Help information for ${moduleName}`]; if (classInfo?.definition) { lines.push(`Class: ${classInfo.definition}`); } if (classInfo?.description) { lines.push(classInfo.description); } if (classInfo?.methodResolutionOrder?.length) { lines.push(`MRO: ${classInfo.methodResolutionOrder.join(" → ")}`); } lines.push(""); if (sections.length > 0) { lines.push(`Sections: ${sections.join(", ")}`, ""); } lines.push(preview); if (memberSummary) { lines.push("", memberSummary); } if (helpText.length > 500) { lines.push("", `💡 Use detailLevel='detailed' to see full documentation (${helpText.length} chars total).`); } return { context: { classInfo, fullLength: helpText.length, helpPreview: preview, members, moduleName, sections, }, text: lines.join("\n"), }; } /** * Detailed mode: Full help text */ function formatDetailed(moduleName, helpText, format) { const title = `Help for ${moduleName}`; const payloadFormat = format ?? DEFAULT_PRESENTER_FORMAT; // For detailed view, return formatted markdown let formatted = `# ${title}\n\n`; formatted += "```\n"; formatted += helpText; formatted += "\n```"; return presentStructuredData({ context: { payloadFormat, title, }, detailLevel: "detailed", structured: { helpText, length: helpText.length, moduleName, }, template: "moduleHelpDetailed", text: formatted, }, payloadFormat); } /** * Build help context */ function buildHelpContext(moduleName, helpText, members, classInfo) { return { classInfo, fullLength: helpText.length, helpPreview: extractHelpPreview(helpText, 200), members, moduleName, sections: extractHelpSections(helpText), }; } /** * Extract preview from help text */ function extractHelpPreview(helpText, maxChars) { const trimmed = helpText.trim(); if (trimmed.length <= maxChars) { return trimmed; } // Try to cut at a natural break point (newline) const firstPart = trimmed.substring(0, maxChars); const lastNewline = firstPart.lastIndexOf("\n"); if (lastNewline > maxChars * 0.7) { return `${firstPart.substring(0, lastNewline)}...`; } return `${firstPart}...`; } /** * Extract section headers from help text */ function extractHelpSections(helpText) { const sections = []; const lines = helpText.split("\n"); // Common help section patterns const sectionPatterns = [ /^([A-Z][A-Za-z\s]+):$/, /^\s*([A-Z][A-Z\s]+)$/, /^-+\s*$/, ]; let lastSection = ""; for (const line of lines) { const trimmed = line.trim(); // Check for section headers for (const pattern of sectionPatterns) { const match = trimmed.match(pattern); if (match?.[1]) { const section = match[1].trim(); if (section && section !== lastSection && section.length < 50) { sections.push(section); lastSection = section; } break; } } // Limit to first 10 sections if (sections.length >= 10) { break; } } return sections; } function extractModuleMembers(helpText) { const methods = []; const properties = []; const seenMethods = new Set(); const seenProperties = new Set(); const lines = helpText.split("\n"); let currentCategory; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; const normalized = trimmed.replace(/^\|/, "").trim(); const headerMatch = normalized.match(/^(.*?):$/); if (headerMatch) { const newCategory = categorizeSection(headerMatch[1]); if (newCategory) { currentCategory = newCategory; } continue; } if (!currentCategory) continue; const entryMatch = trimmed.match(/^\|\s{2,4}([A-Za-z_][\w]*)/); if (!entryMatch) continue; const name = entryMatch[1]; if (!name) continue; if (currentCategory === "method") { if (!seenMethods.has(name)) { seenMethods.add(name); methods.push(name); } } else if (currentCategory === "property") { if (!seenProperties.has(name)) { seenProperties.add(name); properties.push(name); } } } return { methods, properties }; } function extractClassSummary(helpText) { const lines = helpText.split("\n"); let definition; const descriptionLines = []; const methodResolutionOrder = []; let inDescription = false; let inMro = false; for (let i = 0; i < lines.length; i++) { const raw = lines[i]; const trimmed = raw.trim(); if (!definition) { const defMatch = trimmed.match(/^class\s+(.+)$/); if (defMatch) { definition = defMatch[1]; inDescription = true; continue; } } if (inDescription) { if (!trimmed || trimmed.startsWith("| Methods defined here:")) { inDescription = false; continue; // Skip further processing of this line } if (trimmed.startsWith("|")) { const desc = trimmed.replace(/^\|\s*/, ""); if (desc) { descriptionLines.push(desc); } } } if (trimmed.startsWith("| Method resolution order:")) { inMro = true; continue; } if (inMro) { if (!trimmed.startsWith("|")) { // MRO section ended - fall through to the exit check below inMro = false; } else { const entry = trimmed.replace(/^\|\s*/, ""); if (entry) { methodResolutionOrder.push(entry.trim()); } } } // Exit early once we have collected the MRO and we're done with both sections if (methodResolutionOrder.length > 0 && !inMro && !inDescription) { break; } } if (!definition && descriptionLines.length === 0 && methodResolutionOrder.length === 0) { return undefined; } return { definition, description: descriptionLines.slice(0, 3).join(" "), methodResolutionOrder: methodResolutionOrder.length ? methodResolutionOrder : undefined, }; } function categorizeSection(sectionName) { const normalized = sectionName.toLowerCase(); if (normalized.includes("method")) { return "method"; } if (normalized.includes("descriptor") || normalized.includes("attribute") || normalized.includes("property")) { return "property"; } return undefined; } function formatMemberSummary(members, limitPerGroup) { const segments = []; const methodSummary = formatMemberGroup("Methods", members.methods, limitPerGroup); if (methodSummary) { segments.push(methodSummary); } const propertySummary = formatMemberGroup("Properties", members.properties, limitPerGroup); if (propertySummary) { segments.push(propertySummary); } return segments.join("\n\n"); } function formatMemberGroup(label, items, limit) { if (items.length === 0) { return undefined; } const effectiveLimit = typeof limit === "number" && Number.isFinite(limit) ? Math.max(limit, 0) : items.length; const displayed = items.slice(0, effectiveLimit); const suffix = items.length > effectiveLimit ? ", …" : ""; return `${label} (${items.length}): ${displayed.join(", ")}${suffix}`; }