diff --git a/README.md b/README.md index 631c7dc..0a26525 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ npx opendia --port=6000 # Uses 6000 (WebSocket) + 6001 (HTTP) npx opendia --ws-port=5555 --http-port=5556 # Specify individually # Handle port conflicts -npx opendia --kill-existing # Safely terminate existing OpenDia processes +# Note: Existing OpenDia processes are automatically terminated on startup ``` ### Auto-Tunnel Mode diff --git a/opendia-extension/popup.html b/opendia-extension/popup.html index 7683cdc..a466710 100644 --- a/opendia-extension/popup.html +++ b/opendia-extension/popup.html @@ -257,7 +257,7 @@ Start server with: npx opendia Auto-discovery will find the correct ports. - If issues persist, try: npx opendia --kill-existing + Existing processes are automatically terminated on startup diff --git a/opendia-extension/popup.js b/opendia-extension/popup.js index 329b3e0..2cb3ff9 100644 --- a/opendia-extension/popup.js +++ b/opendia-extension/popup.js @@ -84,7 +84,7 @@ function updateStatus(connected) { } else { statusIndicator.className = "status-indicator disconnected"; statusText.innerHTML = `Disconnected from MCP server - Start server with: npx opendia. Auto-discovery will find the correct ports. If issues persist, try: npx opendia --kill-existing`; + Start server with: npx opendia. Auto-discovery will find the correct ports. Existing processes are automatically terminated on startup`; } } diff --git a/opendia-mcp/package-lock.json b/opendia-mcp/package-lock.json index 3d8dca1..8c90f36 100644 --- a/opendia-mcp/package-lock.json +++ b/opendia-mcp/package-lock.json @@ -1,12 +1,12 @@ { "name": "opendia", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opendia", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "dependencies": { "cors": "^2.8.5", diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index 834981b..bfd6966 100755 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -111,14 +111,25 @@ async function handlePortConflict(port, portName) { const isOpenDia = await checkIfOpenDiaProcess(port); if (isOpenDia) { - console.error(`šŸ” Detected existing OpenDia instance on port ${port}`); - console.error(`šŸ’” Options:`); - console.error(` 1. Kill existing: npx opendia --kill-existing`); - console.error(` 2. Use different port: npx opendia --${portName.toLowerCase()}-port=${port + 1}`); - console.error(` 3. Check running processes: lsof -i:${port}`); - console.error(``); - console.error(`ā¹ļø Exiting to avoid conflicts...`); - process.exit(1); + console.error(`šŸ” Detected existing OpenDia instance on port ${port} (this should not happen after cleanup)`); + console.error(`āš ļø Attempting to kill remaining process...`); + await killExistingOpenDia(port); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Check if port is now free + const stillInUse = await checkPortInUse(port); + if (!stillInUse) { + console.error(`āœ… Port ${port} is now available`); + return port; + } + + // If still in use, find alternative port + const altPort = await findAvailablePort(port + 1); + console.error(`šŸ”„ Port ${port} still busy, using port ${altPort}`); + if (portName === 'WebSocket') { + console.error(`šŸ’” Update Chrome extension to: ws://localhost:${altPort}`); + } + return altPort; } else { // Something else is using the port - auto-increment const altPort = await findAvailablePort(port + 1); @@ -270,8 +281,138 @@ async function handleMCPRequest(request) { break; case "prompts/list": - // Return empty prompts list - result = { prompts: [] }; + // Return available workflow prompts + result = { + prompts: [ + { + name: "post_to_social", + description: "Post content to social media platforms with anti-detection bypass", + arguments: [ + { + name: "content", + description: "The content to post", + required: true + }, + { + name: "platform", + description: "Target platform (twitter, linkedin, facebook)", + required: false + } + ] + }, + { + name: "post_selected_quote", + description: "Post currently selected text as a quote with commentary", + arguments: [ + { + name: "commentary", + description: "Your commentary on the selected text", + required: false + } + ] + }, + { + name: "research_workflow", + description: "Research a topic using current page and bookmarking findings", + arguments: [ + { + name: "topic", + description: "Research topic or query", + required: true + }, + { + name: "depth", + description: "Research depth: quick, thorough, comprehensive", + required: false + } + ] + }, + { + name: "analyze_browsing_session", + description: "Analyze current browsing session and provide insights", + arguments: [ + { + name: "focus", + description: "Analysis focus: productivity, research, trends", + required: false + } + ] + }, + { + name: "organize_tabs", + description: "Organize and clean up browser tabs intelligently", + arguments: [ + { + name: "strategy", + description: "Organization strategy: close_duplicates, group_by_domain, archive_old", + required: false + } + ] + }, + { + name: "fill_form_assistant", + description: "Analyze and help fill out forms on the current page", + arguments: [ + { + name: "form_type", + description: "Type of form: contact, registration, survey, application", + required: false + } + ] + } + ] + }; + break; + + case "prompts/get": + // Execute specific workflow based on prompt name + const promptName = params.name; + const promptArgs = params.arguments || {}; + + try { + let workflowResult; + switch (promptName) { + case "post_to_social": + workflowResult = await executePostToSocialWorkflow(promptArgs); + break; + case "post_selected_quote": + workflowResult = await executePostSelectedQuoteWorkflow(promptArgs); + break; + case "research_workflow": + workflowResult = await executeResearchWorkflow(promptArgs); + break; + case "analyze_browsing_session": + workflowResult = await executeSessionAnalysisWorkflow(promptArgs); + break; + case "organize_tabs": + workflowResult = await executeOrganizeTabsWorkflow(promptArgs); + break; + case "fill_form_assistant": + workflowResult = await executeFillFormWorkflow(promptArgs); + break; + default: + throw new Error(`Unknown prompt: ${promptName}`); + } + + result = { + content: [ + { + type: "text", + text: workflowResult + } + ] + }; + } catch (error) { + result = { + content: [ + { + type: "text", + text: `āŒ Workflow execution failed: ${error.message}` + } + ], + isError: true + }; + } break; default: @@ -1395,19 +1536,17 @@ async function startServer() { console.error("šŸš€ Enhanced Browser MCP Server with Anti-Detection Features"); console.error(`šŸ“Š Default ports: WebSocket=${WS_PORT}, HTTP=${HTTP_PORT}`); - // Handle --kill-existing flag - if (killExisting) { - console.error('šŸ”§ Killing existing OpenDia processes...'); - const wsKilled = await killExistingOpenDia(WS_PORT); - const httpKilled = await killExistingOpenDia(HTTP_PORT); - - if (wsKilled || httpKilled) { - console.error('āœ… Existing processes terminated'); - // Wait for ports to be fully released - await new Promise(resolve => setTimeout(resolve, 2000)); - } else { - console.error('ā„¹ļø No existing OpenDia processes found'); - } + // Always kill existing OpenDia processes on startup + console.error('šŸ”§ Checking for existing OpenDia processes...'); + const wsKilled = await killExistingOpenDia(WS_PORT); + const httpKilled = await killExistingOpenDia(HTTP_PORT); + + if (wsKilled || httpKilled) { + console.error('āœ… Existing processes terminated'); + // Wait for ports to be fully released + await new Promise(resolve => setTimeout(resolve, 2000)); + } else { + console.error('ā„¹ļø No existing OpenDia processes found'); } // Resolve port conflicts @@ -1533,7 +1672,7 @@ async function startServer() { console.error(` Current: WebSocket=${WS_PORT}, HTTP=${HTTP_PORT}`); console.error(' Custom: npx opendia --ws-port=6000 --http-port=6001'); console.error(' Or: npx opendia --port=6000 (uses 6000 and 6001)'); - console.error(' Kill existing: npx opendia --kill-existing'); + console.error(' Note: Existing processes are automatically terminated'); console.error(''); } @@ -1551,5 +1690,608 @@ process.on('SIGINT', async () => { process.exit(); }); +// Workflow execution functions +async function executePostToSocialWorkflow(args) { + const { content, platform = "auto" } = args; + + if (!content) { + throw new Error("Content is required for social media posting"); + } + + try { + // Analyze the current page to determine platform and find posting elements + const pageAnalysis = await callBrowserTool('page_analyze', { + intent_hint: 'post_create', + phase: 'discover', + max_results: 3 + }); + + if (!pageAnalysis.elements || pageAnalysis.elements.length === 0) { + throw new Error("No posting elements found on current page. Please navigate to a social media platform."); + } + + // Find the best textarea element for posting + const textareaElement = pageAnalysis.elements.find(el => + el.type === 'textarea' || el.name.toLowerCase().includes('post') || el.name.toLowerCase().includes('tweet') + ); + + if (!textareaElement) { + throw new Error("No suitable posting textarea found on current page"); + } + + // Fill the content using anti-detection bypass + const fillResult = await callBrowserTool('element_fill', { + element_id: textareaElement.id, + value: content, + clear_first: true + }); + + if (!fillResult.success) { + throw new Error(`Failed to fill content: ${fillResult.actual_value}`); + } + + // Look for submit button + const submitElement = pageAnalysis.elements.find(el => + el.type === 'button' && (el.name.toLowerCase().includes('post') || el.name.toLowerCase().includes('tweet') || el.name.toLowerCase().includes('share')) + ); + + let result = `āœ… Successfully posted content to social media!\n\n`; + result += `šŸ“ **Content posted:** "${content}"\n`; + result += `šŸŽÆ **Platform detected:** ${pageAnalysis.summary?.anti_detection_platform || 'Generic'}\n`; + result += `šŸ”§ **Method used:** ${fillResult.method}\n`; + result += `šŸ“Š **Fill success:** ${fillResult.success ? 'Yes' : 'No'}\n`; + + if (submitElement) { + result += `\nšŸ’” **Next step:** Click the "${submitElement.name}" button to publish your post.`; + } else { + result += `\nšŸ’” **Next step:** Look for a "Post" or "Tweet" button to publish your content.`; + } + + return result; + + } catch (error) { + throw new Error(`Social media posting failed: ${error.message}`); + } +} + +async function executePostSelectedQuoteWorkflow(args) { + const { commentary = "" } = args; + + try { + // Get selected text from current page + const selectedText = await callBrowserTool('get_selected_text', { + include_metadata: true, + max_length: 1000 + }); + + if (!selectedText.has_selection) { + throw new Error("No text is currently selected. Please select some text first."); + } + + // Format the quote with commentary + let quoteContent = `"${selectedText.selected_text}"`; + if (selectedText.selection_metadata?.page_info?.title) { + quoteContent += `\n\n— ${selectedText.selection_metadata.page_info.title}`; + } + if (selectedText.selection_metadata?.page_info?.url) { + quoteContent += `\n${selectedText.selection_metadata.page_info.url}`; + } + if (commentary) { + quoteContent += `\n\n${commentary}`; + } + + // Execute the post workflow with the formatted quote + const postResult = await executePostToSocialWorkflow({ content: quoteContent }); + + let result = `šŸŽÆ **Selected Quote Posting Workflow**\n\n`; + result += `šŸ“ **Selected text:** "${selectedText.selected_text.substring(0, 100)}${selectedText.selected_text.length > 100 ? '...' : ''}"\n`; + result += `šŸ“„ **Source:** ${selectedText.selection_metadata?.page_info?.title || 'Current page'}\n`; + result += `šŸ’¬ **Commentary:** ${commentary || 'None'}\n`; + result += `šŸ“Š **Character count:** ${quoteContent.length}\n\n`; + result += postResult; + + return result; + + } catch (error) { + throw new Error(`Quote posting workflow failed: ${error.message}`); + } +} + +async function executeResearchWorkflow(args) { + const { topic, depth = "thorough" } = args; + + if (!topic) { + throw new Error("Research topic is required"); + } + + try { + // Analyze current page content + const pageContent = await callBrowserTool('page_extract_content', { + content_type: 'article', + summarize: true + }); + + // Get current page links for related research + const pageLinks = await callBrowserTool('get_page_links', { + include_internal: true, + include_external: true, + max_results: 20 + }); + + // Search browsing history for related content + const historyResults = await callBrowserTool('get_history', { + keywords: topic, + max_results: 10, + sort_by: 'visit_time' + }); + + // Bookmark current page if it has relevant content - URL will be obtained from browser extension + const currentUrl = pageContent.content?.url; + const currentTitle = pageContent.summary?.title; + + if (currentUrl && currentTitle) { + try { + await callBrowserTool('add_bookmark', { + title: `[Research: ${topic}] ${currentTitle}`, + url: currentUrl + }); + } catch (bookmarkError) { + console.warn('Bookmark creation failed:', bookmarkError.message); + } + } + + // Compile research summary + let result = `šŸ” **Research Workflow: ${topic}**\n\n`; + + // Current page analysis + result += `šŸ“„ **Current Page Analysis:**\n`; + if (pageContent.summary) { + result += `• **Title:** ${pageContent.summary.title || 'N/A'}\n`; + result += `• **Word count:** ${pageContent.summary.word_count || 0}\n`; + result += `• **Reading time:** ${pageContent.summary.reading_time || 0} minutes\n`; + result += `• **Has media:** ${pageContent.summary.has_images || pageContent.summary.has_videos ? 'Yes' : 'No'}\n`; + if (pageContent.summary.preview) { + result += `• **Preview:** ${pageContent.summary.preview}\n`; + } + } + + // Related links + result += `\nšŸ”— **Related Links Found:** ${pageLinks.returned}\n`; + const relevantLinks = pageLinks.links.filter(link => + link.text.toLowerCase().includes(topic.toLowerCase()) || + link.url.toLowerCase().includes(topic.toLowerCase()) + ).slice(0, 5); + + if (relevantLinks.length > 0) { + result += `**Top relevant links:**\n`; + relevantLinks.forEach((link, index) => { + result += `${index + 1}. [${link.text}](${link.url})\n`; + }); + } + + // History analysis + result += `\nšŸ“š **Previous Research:**\n`; + if (historyResults.history_items && historyResults.history_items.length > 0) { + result += `Found ${historyResults.history_items.length} related pages in your history:\n`; + historyResults.history_items.slice(0, 5).forEach((item, index) => { + result += `${index + 1}. **${item.title}** (visited ${item.visit_count} times)\n`; + result += ` ${item.url}\n`; + }); + } else { + result += `No previous research found in browsing history.\n`; + } + + // Research recommendations + result += `\nšŸ’” **Next Steps:**\n`; + if (depth === "comprehensive") { + result += `• Explore the ${pageLinks.returned} links found on current page\n`; + result += `• Cross-reference with ${historyResults.metadata?.total_found || 0} historical visits\n`; + result += `• Consider bookmarking additional relevant pages\n`; + } else if (depth === "thorough") { + result += `• Review top ${Math.min(5, pageLinks.returned)} most relevant links\n`; + result += `• Check recent history for related content\n`; + } else { + result += `• Focus on current page content and top 3 related links\n`; + } + + result += `\nāœ… **Current page bookmarked for reference**`; + + return result; + + } catch (error) { + throw new Error(`Research workflow failed: ${error.message}`); + } +} + +async function executeSessionAnalysisWorkflow(args) { + const { focus = "productivity" } = args; + + try { + // Get all open tabs + const tabList = await callBrowserTool('tab_list', { + current_window_only: false, + include_details: true + }); + + // Get recent browsing history + const recentHistory = await callBrowserTool('get_history', { + max_results: 50, + sort_by: 'visit_time', + sort_order: 'desc' + }); + + // Analyze current page + const currentPageContent = await callBrowserTool('page_extract_content', { + content_type: 'article', + summarize: true + }); + + // Process tabs data + const tabs = tabList.tabs || []; + const domains = [...new Set(tabs.map(tab => { + try { + return new URL(tab.url).hostname; + } catch { + return 'unknown'; + } + }))]; + + // Categorize tabs by domain type + const socialMediaDomains = ['twitter.com', 'x.com', 'linkedin.com', 'facebook.com', 'instagram.com']; + const productivityDomains = ['docs.google.com', 'notion.so', 'obsidian.md', 'github.com']; + const newsDomains = ['news.google.com', 'bbc.com', 'cnn.com', 'reuters.com']; + + const categorizedTabs = { + social: tabs.filter(tab => socialMediaDomains.some(domain => tab.url.includes(domain))), + productivity: tabs.filter(tab => productivityDomains.some(domain => tab.url.includes(domain))), + news: tabs.filter(tab => newsDomains.some(domain => tab.url.includes(domain))), + other: tabs.filter(tab => + !socialMediaDomains.some(domain => tab.url.includes(domain)) && + !productivityDomains.some(domain => tab.url.includes(domain)) && + !newsDomains.some(domain => tab.url.includes(domain)) + ) + }; + + // Compile analysis + let result = `šŸ“Š **Browsing Session Analysis**\n\n`; + + // Session overview + result += `šŸŽÆ **Session Overview:**\n`; + result += `• **Total open tabs:** ${tabs.length}\n`; + result += `• **Unique domains:** ${domains.length}\n`; + result += `• **Active tab:** ${tabList.active_tab ? 'Yes' : 'No'}\n`; + result += `• **Recent history items:** ${recentHistory.metadata?.total_found || 0}\n`; + + // Tab categorization + result += `\nšŸ“‚ **Tab Categories:**\n`; + result += `• **Social Media:** ${categorizedTabs.social.length} tabs\n`; + result += `• **Productivity:** ${categorizedTabs.productivity.length} tabs\n`; + result += `• **News/Information:** ${categorizedTabs.news.length} tabs\n`; + result += `• **Other:** ${categorizedTabs.other.length} tabs\n`; + + // Domain analysis + result += `\n🌐 **Top Domains:**\n`; + const domainCounts = {}; + tabs.forEach(tab => { + try { + const domain = new URL(tab.url).hostname; + domainCounts[domain] = (domainCounts[domain] || 0) + 1; + } catch {} + }); + + Object.entries(domainCounts) + .sort(([,a], [,b]) => b - a) + .slice(0, 5) + .forEach(([domain, count]) => { + result += `• **${domain}:** ${count} tab${count > 1 ? 's' : ''}\n`; + }); + + // Focus-specific analysis + if (focus === "productivity") { + result += `\nšŸ’¼ **Productivity Analysis:**\n`; + const duplicateTabs = tabs.filter((tab, index) => + tabs.findIndex(t => t.url === tab.url) !== index + ); + result += `• **Duplicate tabs:** ${duplicateTabs.length}\n`; + result += `• **Productivity tools:** ${categorizedTabs.productivity.length}\n`; + result += `• **Social media distractions:** ${categorizedTabs.social.length}\n`; + + if (categorizedTabs.productivity.length > 0) { + result += `\n**Active productivity tools:**\n`; + categorizedTabs.productivity.slice(0, 3).forEach(tab => { + result += `• ${tab.title}\n`; + }); + } + } else if (focus === "research") { + result += `\nšŸ” **Research Analysis:**\n`; + result += `• **Information sources:** ${categorizedTabs.news.length + categorizedTabs.other.length}\n`; + result += `• **Research depth:** ${recentHistory.metadata?.total_found > 20 ? 'Deep' : 'Surface'}\n`; + + if (currentPageContent.summary) { + result += `• **Current page type:** ${currentPageContent.content_type || 'Unknown'}\n`; + result += `• **Reading time:** ${currentPageContent.summary.reading_time || 0} minutes\n`; + } + } + + // Recommendations + result += `\nšŸ’” **Recommendations:**\n`; + if (tabs.length > 20) { + result += `• Consider closing some tabs to improve performance\n`; + } + if (domainCounts['twitter.com'] > 3 || domainCounts['x.com'] > 3) { + result += `• Multiple social media tabs detected - consider consolidating\n`; + } + if (categorizedTabs.productivity.length > 0 && categorizedTabs.social.length > 0) { + result += `• Mix of productivity and social tabs - consider separate browsing sessions\n`; + } + + result += `\nšŸ“ˆ **Session Score:** ${Math.round(((categorizedTabs.productivity.length + categorizedTabs.news.length) / tabs.length) * 100)}% productive`; + + return result; + + } catch (error) { + throw new Error(`Session analysis workflow failed: ${error.message}`); + } +} + +async function executeOrganizeTabsWorkflow(args) { + const { strategy = "close_duplicates" } = args; + + try { + // Get all open tabs + const tabList = await callBrowserTool('tab_list', { + current_window_only: false, + include_details: true + }); + + const tabs = tabList.tabs || []; + let result = `šŸ—‚ļø **Tab Organization Workflow**\n\n`; + result += `šŸ“Š **Starting with ${tabs.length} tabs**\n\n`; + + let closedTabs = []; + let organizedTabs = []; + + if (strategy === "close_duplicates") { + // Find and close duplicate tabs + const seenUrls = new Set(); + const duplicates = []; + + tabs.forEach(tab => { + if (seenUrls.has(tab.url)) { + duplicates.push(tab); + } else { + seenUrls.add(tab.url); + } + }); + + // Close duplicate tabs (keep the first occurrence) + if (duplicates.length > 0) { + const tabIds = duplicates.map(tab => tab.id); + const closeResult = await callBrowserTool('tab_close', { + tab_ids: tabIds + }); + + if (closeResult.success) { + closedTabs = duplicates; + result += `āœ… **Closed ${duplicates.length} duplicate tabs:**\n`; + duplicates.forEach(tab => { + result += `• ${tab.title}\n`; + }); + } + } else { + result += `āœ… **No duplicate tabs found**\n`; + } + + } else if (strategy === "group_by_domain") { + // Group tabs by domain + const domainGroups = {}; + tabs.forEach(tab => { + try { + const domain = new URL(tab.url).hostname; + if (!domainGroups[domain]) { + domainGroups[domain] = []; + } + domainGroups[domain].push(tab); + } catch { + if (!domainGroups['unknown']) { + domainGroups['unknown'] = []; + } + domainGroups['unknown'].push(tab); + } + }); + + result += `šŸ“‚ **Grouped tabs by domain:**\n`; + Object.entries(domainGroups).forEach(([domain, domainTabs]) => { + result += `• **${domain}:** ${domainTabs.length} tabs\n`; + domainTabs.slice(0, 3).forEach(tab => { + result += ` - ${tab.title}\n`; + }); + if (domainTabs.length > 3) { + result += ` - ... and ${domainTabs.length - 3} more\n`; + } + }); + + } else if (strategy === "archive_old") { + // Find tabs that haven't been active recently + const currentTime = Date.now(); + const oneHourAgo = currentTime - (60 * 60 * 1000); + + // Since we don't have last accessed time, we'll use a heuristic + // based on tab loading status and position + const staleTabsToClose = tabs.filter(tab => + tab.status === 'complete' && + !tab.active && + !tab.pinned && + tab.index > 10 // Assume tabs at the end are less active + ).slice(0, 10); // Limit to 10 tabs max + + if (staleTabsToClose.length > 0) { + const tabIds = staleTabsToClose.map(tab => tab.id); + const closeResult = await callBrowserTool('tab_close', { + tab_ids: tabIds + }); + + if (closeResult.success) { + closedTabs = staleTabsToClose; + result += `āœ… **Archived ${staleTabsToClose.length} old tabs:**\n`; + staleTabsToClose.forEach(tab => { + result += `• ${tab.title}\n`; + }); + } + } else { + result += `āœ… **No old tabs to archive**\n`; + } + } + + // Final summary + const remainingTabs = tabs.length - closedTabs.length; + result += `\nšŸ“ˆ **Organization Results:**\n`; + result += `• **Tabs closed:** ${closedTabs.length}\n`; + result += `• **Tabs remaining:** ${remainingTabs}\n`; + result += `• **Organization strategy:** ${strategy}\n`; + + if (remainingTabs > 15) { + result += `\nšŸ’” **Recommendation:** Consider running additional organization strategies to further reduce tab count.`; + } else { + result += `\nāœ… **Tab organization complete!** Your browsing session is now more organized.`; + } + + return result; + + } catch (error) { + throw new Error(`Tab organization workflow failed: ${error.message}`); + } +} + +async function executeFillFormWorkflow(args) { + const { form_type = "auto" } = args; + + try { + // Analyze page for form elements + const formAnalysis = await callBrowserTool('page_analyze', { + intent_hint: 'form submit', + phase: 'detailed', + focus_areas: ['forms', 'buttons'], + max_results: 10 + }); + + if (!formAnalysis.elements || formAnalysis.elements.length === 0) { + throw new Error("No form elements found on current page"); + } + + // Categorize form elements + const formElements = { + inputs: formAnalysis.elements.filter(el => el.type === 'input'), + textareas: formAnalysis.elements.filter(el => el.type === 'textarea'), + selects: formAnalysis.elements.filter(el => el.type === 'select'), + buttons: formAnalysis.elements.filter(el => el.type === 'button') + }; + + // Analyze each form element for type and requirements + let result = `šŸ“ **Form Analysis & Fill Assistant**\n\n`; + + // Form overview + result += `šŸ” **Form Elements Found:**\n`; + result += `• **Input fields:** ${formElements.inputs.length}\n`; + result += `• **Text areas:** ${formElements.textareas.length}\n`; + result += `• **Select dropdowns:** ${formElements.selects.length}\n`; + result += `• **Buttons:** ${formElements.buttons.length}\n`; + + // Detailed element analysis + if (formElements.inputs.length > 0) { + result += `\nšŸ“Š **Input Field Analysis:**\n`; + formElements.inputs.forEach((input, index) => { + const elementState = formAnalysis.elements.find(el => el.id === input.id); + result += `${index + 1}. **${input.name}**\n`; + result += ` • Element ID: ${input.id}\n`; + result += ` • Ready: ${elementState?.ready ? 'Yes' : 'No'}\n`; + result += ` • Required: ${input.name.includes('*') ? 'Yes' : 'Unknown'}\n`; + + // Suggest field type based on name + const fieldName = input.name.toLowerCase(); + if (fieldName.includes('email')) { + result += ` • **Suggested type:** Email address\n`; + } else if (fieldName.includes('name')) { + result += ` • **Suggested type:** Name field\n`; + } else if (fieldName.includes('phone')) { + result += ` • **Suggested type:** Phone number\n`; + } else if (fieldName.includes('password')) { + result += ` • **Suggested type:** Password\n`; + } else { + result += ` • **Suggested type:** General text input\n`; + } + }); + } + + // Text area analysis + if (formElements.textareas.length > 0) { + result += `\nšŸ“ **Text Area Analysis:**\n`; + formElements.textareas.forEach((textarea, index) => { + result += `${index + 1}. **${textarea.name}**\n`; + result += ` • Element ID: ${textarea.id}\n`; + result += ` • **Suggested use:** Long-form text input\n`; + }); + } + + // Submit buttons + if (formElements.buttons.length > 0) { + result += `\nšŸ”˜ **Submit Buttons:**\n`; + const submitButtons = formElements.buttons.filter(btn => + btn.name.toLowerCase().includes('submit') || + btn.name.toLowerCase().includes('send') || + btn.name.toLowerCase().includes('save') + ); + + submitButtons.forEach((button, index) => { + result += `${index + 1}. **${button.name}** (ID: ${button.id})\n`; + }); + } + + // Form type detection + result += `\nšŸŽÆ **Detected Form Type:**\n`; + // Note: Page content detection would need to be done through browser tools + const contentLower = ''; + + let detectedType = 'unknown'; + if (contentLower.includes('contact') || contentLower.includes('get in touch')) { + detectedType = 'contact'; + } else if (contentLower.includes('register') || contentLower.includes('sign up')) { + detectedType = 'registration'; + } else if (contentLower.includes('survey') || contentLower.includes('feedback')) { + detectedType = 'survey'; + } else if (contentLower.includes('application') || contentLower.includes('apply')) { + detectedType = 'application'; + } + + result += `• **Auto-detected:** ${detectedType}\n`; + result += `• **User specified:** ${form_type}\n`; + + // Filling recommendations + result += `\nšŸ’” **Filling Recommendations:**\n`; + if (formElements.inputs.length > 0) { + result += `• Start with required fields (marked with *)\n`; + result += `• Use the element IDs provided for precise filling\n`; + result += `• Test form validation before final submission\n`; + } + + // Ready-to-fill elements + const readyElements = formAnalysis.elements.filter(el => el.ready); + result += `\nāœ… **Ready to Fill:** ${readyElements.length} elements are ready for interaction\n`; + + if (readyElements.length > 0) { + result += `**Next steps:**\n`; + result += `1. Use element_fill with the provided Element IDs\n`; + result += `2. Fill required fields first\n`; + result += `3. Review form before submission\n`; + result += `4. Click appropriate submit button when ready\n`; + } + + return result; + + } catch (error) { + throw new Error(`Form analysis workflow failed: ${error.message}`); + } +} + // Start the server startServer();