opendia
Version:
šÆ OpenDia - The open alternative to Dia. Connect your browser to AI models with anti-detection bypass for Twitter/X, LinkedIn, Facebook
1,499 lines (1,348 loc) ⢠85.5 kB
JavaScript
#!/usr/bin/env node
const WebSocket = require("ws");
const express = require("express");
const net = require('net');
const { exec } = require('child_process');
// ADD: New imports for SSE transport
const cors = require('cors');
const { createServer } = require('http');
const { spawn } = require('child_process');
// ADD: Enhanced command line argument parsing
const args = process.argv.slice(2);
const enableTunnel = args.includes('--tunnel') || args.includes('--auto-tunnel');
const sseOnly = args.includes('--sse-only');
const killExisting = args.includes('--kill-existing');
// Parse port arguments
const wsPortArg = args.find(arg => arg.startsWith('--ws-port='));
const httpPortArg = args.find(arg => arg.startsWith('--http-port='));
const portArg = args.find(arg => arg.startsWith('--port='));
// Default ports (changed from 3000/3001 to 5555/5556)
let WS_PORT = wsPortArg ? parseInt(wsPortArg.split('=')[1]) : (portArg ? parseInt(portArg.split('=')[1]) : 5555);
let HTTP_PORT = httpPortArg ? parseInt(httpPortArg.split('=')[1]) : (portArg ? parseInt(portArg.split('=')[1]) + 1 : 5556);
// Port conflict detection utilities
async function checkPortInUse(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.listen(port, () => {
server.once('close', () => resolve(false));
server.close();
});
server.on('error', () => resolve(true));
});
}
async function checkIfOpenDiaProcess(port) {
return new Promise((resolve) => {
exec(`lsof -ti:${port}`, (error, stdout) => {
if (error || !stdout.trim()) {
resolve(false);
return;
}
const pid = stdout.trim().split('\n')[0];
exec(`ps -p ${pid} -o command=`, (psError, psOutput) => {
resolve(!psError && (
psOutput.includes('opendia') ||
psOutput.includes('server.js') ||
psOutput.includes('node') && psOutput.includes('opendia')
));
});
});
});
}
async function findAvailablePort(startPort) {
let port = startPort;
while (await checkPortInUse(port)) {
port++;
if (port > startPort + 100) { // Safety limit
throw new Error(`Could not find available port after checking ${port - startPort} ports`);
}
}
return port;
}
async function killExistingOpenDia(port) {
return new Promise((resolve) => {
exec(`lsof -ti:${port}`, async (error, stdout) => {
if (error || !stdout.trim()) {
resolve(false);
return;
}
const pids = stdout.trim().split('\n');
let killedAny = false;
for (const pid of pids) {
const isOpenDia = await checkIfOpenDiaProcess(port);
if (isOpenDia) {
exec(`kill ${pid}`, (killError) => {
if (!killError) {
console.error(`š§ Killed existing OpenDia process (PID: ${pid})`);
killedAny = true;
}
});
}
}
// Wait a moment for processes to fully exit
setTimeout(() => resolve(killedAny), 1000);
});
});
}
async function handlePortConflict(port, portName) {
const isInUse = await checkPortInUse(port);
if (!isInUse) {
return port; // Port is free, use it
}
// Port is busy - give user options
console.error(`ā ļø ${portName} port ${port} is already in use`);
// Check if it's likely another OpenDia instance
const isOpenDia = await checkIfOpenDiaProcess(port);
if (isOpenDia) {
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/Firefox extension to: ws://localhost:${altPort}`);
}
return altPort;
} else {
// Something else is using the port - auto-increment
const altPort = await findAvailablePort(port + 1);
console.error(`š ${portName} port ${port} busy (non-OpenDia), using port ${altPort}`);
if (portName === 'WebSocket') {
console.error(`š” Update Chrome/Firefox extension to: ws://localhost:${altPort}`);
}
return altPort;
}
}
// ADD: Express app setup
const app = express();
app.use(cors());
app.use(express.json());
// WebSocket server for Chrome/Firefox Extension (will be initialized after port conflict resolution)
let wss = null;
let chromeExtensionSocket = null;
let availableTools = [];
// Tool call tracking
const pendingCalls = new Map();
// Simple MCP protocol implementation over stdio
async function handleMCPRequest(request) {
const { method, params, id } = request;
// Handle notifications (no id means it's a notification)
if (!id && method && method.startsWith("notifications/")) {
console.error(`Received notification: ${method}`);
return null; // No response needed for notifications
}
// Handle requests that don't need implementation
if (id === undefined || id === null) {
return null; // No response for notifications
}
try {
let result;
switch (method) {
case "initialize":
// RESPOND IMMEDIATELY - don't wait for extension
console.error(
`MCP client initializing: ${params?.clientInfo?.name || "unknown"}`
);
result = {
protocolVersion: "2024-11-05",
capabilities: {
tools: {},
},
serverInfo: {
name: "browser-mcp-server",
version: "2.0.0",
},
instructions:
"šÆ Enhanced browser automation with anti-detection bypass for Twitter/X, LinkedIn, Facebook. Extension may take a moment to connect.",
};
break;
case "tools/list":
// Debug logging
console.error(
`Tools/list called. Extension connected: ${
chromeExtensionSocket &&
chromeExtensionSocket.readyState === WebSocket.OPEN
}, Available tools: ${availableTools.length}`
);
// Return tools from extension if available, otherwise fallback tools
if (
chromeExtensionSocket &&
chromeExtensionSocket.readyState === WebSocket.OPEN &&
availableTools.length > 0
) {
console.error(
`Returning ${availableTools.length} tools from extension`
);
result = {
tools: availableTools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
} else {
// Return basic fallback tools
console.error("Extension not connected, returning fallback tools");
result = {
tools: getFallbackTools(),
};
}
break;
case "tools/call":
if (
!chromeExtensionSocket ||
chromeExtensionSocket.readyState !== WebSocket.OPEN
) {
// Extension not connected - return helpful error
result = {
content: [
{
type: "text",
text: "ā Browser Extension not connected. Please install and activate the browser extension, then try again.\n\nSetup instructions:\n\nFor Chrome: \n1. Go to chrome://extensions/\n2. Enable Developer mode\n3. Click 'Load unpacked' and select the Chrome extension folder\n\nFor Firefox:\n1. Go to about:debugging#/runtime/this-firefox\n2. Click 'Load Temporary Add-on...'\n3. Select the manifest-firefox.json file\n\nšÆ Features: Anti-detection bypass for Twitter/X, LinkedIn, Facebook + universal automation",
},
],
isError: true,
};
} else {
// Extension connected - try the tool call
try {
const toolResult = await callBrowserTool(
params.name,
params.arguments || {}
);
// Format response based on tool type
const formattedResult = formatToolResult(params.name, toolResult);
result = {
content: [
{
type: "text",
text: formattedResult,
},
],
isError: false,
};
} catch (error) {
result = {
content: [
{
type: "text",
text: `ā Tool execution failed: ${error.message}`,
},
],
isError: true,
};
}
}
break;
case "resources/list":
// Return empty resources list
result = { resources: [] };
break;
case "prompts/list":
// 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:
throw new Error(`Unknown method: ${method}`);
}
return { jsonrpc: "2.0", id, result };
} catch (error) {
return {
jsonrpc: "2.0",
id,
error: {
code: -32603,
message: error.message,
},
};
}
}
// Enhanced tool result formatting with anti-detection support
function formatToolResult(toolName, result) {
const metadata = {
tool: toolName,
execution_time: result.execution_time || 0,
timestamp: new Date().toISOString(),
};
switch (toolName) {
case "page_analyze":
return formatPageAnalyzeResult(result, metadata);
case "page_extract_content":
return formatContentExtractionResult(result, metadata);
case "element_click":
return formatElementClickResult(result, metadata);
case "element_fill":
return formatElementFillResult(result, metadata);
case "page_navigate":
return `ā
Successfully navigated to: ${
result.url || "unknown URL"
}\n\n${JSON.stringify(metadata, null, 2)}`;
case "page_wait_for":
return (
`ā
Condition met: ${result.condition_type || "unknown"}\n` +
`Wait time: ${result.wait_time || 0}ms\n\n${JSON.stringify(
metadata,
null,
2
)}`
);
case "get_history":
return formatHistoryResult(result, metadata);
case "get_selected_text":
return formatSelectedTextResult(result, metadata);
case "page_scroll":
return formatScrollResult(result, metadata);
case "get_page_links":
return formatLinksResult(result, metadata);
case "tab_create":
return formatTabCreateResult(result, metadata);
case "tab_close":
return formatTabCloseResult(result, metadata);
case "tab_list":
return formatTabListResult(result, metadata);
case "tab_switch":
return formatTabSwitchResult(result, metadata);
case "element_get_state":
return formatElementStateResult(result, metadata);
case "page_style":
return formatPageStyleResult(result, metadata);
default:
// Legacy tools or unknown tools
return JSON.stringify(result, null, 2);
}
}
function formatPageAnalyzeResult(result, metadata) {
if (result.elements && result.elements.length > 0) {
const platformInfo = result.summary?.anti_detection_platform
? `\nšÆ Anti-detection platform detected: ${result.summary.anti_detection_platform}`
: "";
const summary =
`Found ${result.elements.length} relevant elements using ${result.method}:${platformInfo}\n\n` +
result.elements
.map((el) => {
const readyStatus = el.ready ? "ā
Ready" : "ā ļø Not ready";
const stateInfo = el.state === "disabled" ? " (disabled)" : "";
return `⢠${el.name} (${el.type}) - Confidence: ${el.conf}% ${readyStatus}${stateInfo}\n Element ID: ${el.id}`;
})
.join("\n\n");
return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`;
} else {
const intentHint = result.intent_hint || "unknown";
const platformInfo = result.summary?.anti_detection_platform
? `\nPlatform: ${result.summary.anti_detection_platform}`
: "";
return `No relevant elements found for intent: "${intentHint}"${platformInfo}\n\n${JSON.stringify(
metadata,
null,
2
)}`;
}
}
function formatContentExtractionResult(result, metadata) {
const contentSummary = `Extracted ${result.content_type} content using ${result.method}:\n\n`;
if (result.content) {
// Check if this is full content extraction (summarize=false) or summary
// If it's a content object with properties, show full content
// If it's a string or small content, it's probably summarized
let preview;
if (typeof result.content === "string") {
// String content - likely summarized, keep truncation
preview = result.content.substring(0, 500) + (result.content.length > 500 ? "..." : "");
} else if (result.content && typeof result.content === "object") {
// Object content - check if it's full content extraction
if (result.content.content && result.content.content.length > 1000) {
// This looks like full content extraction - don't truncate
preview = JSON.stringify(result.content, null, 2);
} else {
// Smaller content, apply truncation
preview = JSON.stringify(result.content, null, 2).substring(0, 500);
}
} else {
// Fallback
preview = JSON.stringify(result.content, null, 2).substring(0, 500);
}
return `${contentSummary}${preview}\n\n${JSON.stringify(
metadata,
null,
2
)}`;
} else if (result.summary) {
// Enhanced summarized content response
const summaryText = formatContentSummary(
result.summary,
result.content_type
);
return `${contentSummary}${summaryText}\n\n${JSON.stringify(
metadata,
null,
2
)}`;
} else {
return `${contentSummary}No content found\n\n${JSON.stringify(
metadata,
null,
2
)}`;
}
}
function formatContentSummary(summary, contentType) {
switch (contentType) {
case "article":
return (
`š° Article: "${summary.title}"\n` +
`š Word count: ${summary.word_count}\n` +
`ā±ļø Reading time: ${summary.reading_time} minutes\n` +
`š¼ļø Has media: ${summary.has_images || summary.has_videos}\n` +
`Preview: ${summary.preview}`
);
case "search_results":
return (
`š Search Results Summary:\n` +
`š Total results: ${summary.total_results}\n` +
`š Quality score: ${summary.quality_score}/100\n` +
`š Average relevance: ${Math.round(summary.avg_score * 100)}%\n` +
`š Top domains: ${summary.top_domains
?.map((d) => d.domain)
.join(", ")}\n` +
`š Result types: ${summary.result_types?.join(", ")}`
);
case "posts":
return (
`š± Social Posts Summary:\n` +
`š Post count: ${summary.post_count}\n` +
`š Average length: ${summary.avg_length} characters\n` +
`ā¤ļø Total engagement: ${summary.engagement_total}\n` +
`š¼ļø Posts with media: ${summary.has_media_count}\n` +
`š„ Unique authors: ${summary.authors}\n` +
`š Post types: ${summary.post_types?.join(", ")}`
);
default:
return JSON.stringify(summary, null, 2);
}
}
function formatElementClickResult(result, metadata) {
return (
`ā
Successfully clicked element: ${
result.element_name || result.element_id
}\n` +
`Click type: ${result.click_type || "left"}\n\n${JSON.stringify(
metadata,
null,
2
)}`
);
}
function formatElementFillResult(result, metadata) {
// Enhanced formatting for anti-detection bypass methods
const methodEmojis = {
twitter_direct_bypass: "š¦ Twitter Direct Bypass",
linkedin_direct_bypass: "š¼ LinkedIn Direct Bypass",
facebook_direct_bypass: "š Facebook Direct Bypass",
generic_direct_bypass: "šÆ Generic Direct Bypass",
standard_fill: "š§ Standard Fill",
anti_detection_bypass: "š”ļø Anti-Detection Bypass",
};
const methodDisplay = methodEmojis[result.method] || result.method;
const successIcon = result.success ? "ā
" : "ā";
let fillResult = `${successIcon} Element fill ${
result.success ? "completed" : "failed"
} using ${methodDisplay}\n`;
fillResult += `š Target: ${result.element_name || result.element_id}\n`;
fillResult += `š¬ Input: "${result.value}"\n`;
if (result.actual_value) {
fillResult += `š Result: "${result.actual_value}"\n`;
}
// Add bypass-specific information
if (
result.method?.includes("bypass") &&
result.execCommand_result !== undefined
) {
fillResult += `š§ execCommand success: ${result.execCommand_result}\n`;
}
if (!result.success && result.method?.includes("bypass")) {
fillResult += `\nā ļø Direct bypass failed - page may have enhanced detection. Try refreshing the page.\n`;
}
return `${fillResult}\n${JSON.stringify(metadata, null, 2)}`;
}
function formatHistoryResult(result, metadata) {
if (!result.history_items || result.history_items.length === 0) {
return `š No history items found matching the criteria\n\n${JSON.stringify(metadata, null, 2)}`;
}
const summary = `š Found ${result.history_items.length} history items (${result.metadata.total_found} total matches):\n\n`;
const items = result.history_items.map((item, index) => {
const visitInfo = `Visits: ${item.visit_count}`;
const timeInfo = new Date(item.last_visit_time).toLocaleDateString();
const domainInfo = `[${item.domain}]`;
return `${index + 1}. **${item.title}**\n ${domainInfo} ${visitInfo} | Last: ${timeInfo}\n URL: ${item.url}`;
}).join('\n\n');
const searchSummary = result.metadata.search_params.keywords ?
`\nš Search: "${result.metadata.search_params.keywords}"` : '';
const dateSummary = result.metadata.search_params.date_range ?
`\nš
Date range: ${result.metadata.search_params.date_range}` : '';
const domainSummary = result.metadata.search_params.domains ?
`\nš Domains: ${result.metadata.search_params.domains.join(', ')}` : '';
const visitSummary = result.metadata.search_params.min_visit_count > 1 ?
`\nš Min visits: ${result.metadata.search_params.min_visit_count}` : '';
return `${summary}${items}${searchSummary}${dateSummary}${domainSummary}${visitSummary}\n\n${JSON.stringify(metadata, null, 2)}`;
}
function formatSelectedTextResult(result, metadata) {
if (!result.has_selection) {
return `š No text selected\n\n${result.message || "No text is currently selected on the page"}\n\n${JSON.stringify(metadata, null, 2)}`;
}
const textPreview = result.selected_text.length > 200
? result.selected_text.substring(0, 200) + "..."
: result.selected_text;
let summary = `š Selected Text (${result.character_count} characters):\n\n"${textPreview}"`;
if (result.truncated) {
summary += `\n\nā ļø Text was truncated to fit length limit`;
}
if (result.selection_metadata) {
const meta = result.selection_metadata;
summary += `\n\nš Selection Details:`;
summary += `\n⢠Word count: ${meta.word_count}`;
summary += `\n⢠Line count: ${meta.line_count}`;
summary += `\n⢠Position: ${Math.round(meta.position.x)}, ${Math.round(meta.position.y)}`;
if (meta.parent_element.tag_name) {
summary += `\n⢠Parent element: <${meta.parent_element.tag_name}>`;
if (meta.parent_element.class_name) {
summary += ` class="${meta.parent_element.class_name}"`;
}
}
if (meta.page_info) {
summary += `\n⢠Page: ${meta.page_info.title}`;
summary += `\n⢠Domain: ${meta.page_info.domain}`;
}
}
return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`;
}
function formatScrollResult(result, metadata) {
if (!result.success) {
return `š Scroll failed: ${result.error || "Unknown error"}\n\n${JSON.stringify(metadata, null, 2)}`;
}
let summary = `š Page scrolled successfully`;
if (result.direction) {
summary += ` ${result.direction}`;
}
if (result.amount && result.amount !== "custom") {
summary += ` (${result.amount})`;
} else if (result.pixels) {
summary += ` (${result.pixels}px)`;
}
if (result.element_scrolled) {
summary += `\nšÆ Scrolled to element: ${result.element_scrolled}`;
}
if (result.scroll_position) {
summary += `\nš New position: x=${result.scroll_position.x}, y=${result.scroll_position.y}`;
}
if (result.page_dimensions) {
const { width, height, scrollWidth, scrollHeight } = result.page_dimensions;
summary += `\nš Page size: ${width}x${height} (scrollable: ${scrollWidth}x${scrollHeight})`;
}
if (result.wait_time) {
summary += `\nā±ļø Waited ${result.wait_time}ms after scroll`;
}
return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`;
}
function formatLinksResult(result, metadata) {
if (!result.links || result.links.length === 0) {
return `š No links found on the page\n\n${JSON.stringify(metadata, null, 2)}`;
}
const summary = `š Found ${result.returned} links (${result.total_found} total on page):\n`;
const currentDomain = result.current_domain ? `\nš Current domain: ${result.current_domain}` : '';
const linksList = result.links.map((link, index) => {
const typeIcon = link.type === 'internal' ? 'š ' : 'š';
const linkText = link.text.length > 50 ? link.text.substring(0, 50) + '...' : link.text;
const displayText = linkText || '[No text]';
const title = link.title ? `\n Title: ${link.title}` : '';
const domain = link.domain ? ` [${link.domain}]` : '';
return `${index + 1}. ${typeIcon} **${displayText}**${domain}${title}\n URL: ${link.url}`;
}).join('\n\n');
const filterInfo = [];
if (result.links.some(l => l.type === 'internal') && result.links.some(l => l.type === 'external')) {
const internal = result.links.filter(l => l.type === 'internal').length;
const external = result.links.filter(l => l.type === 'external').length;
filterInfo.push(`š Internal: ${internal}, External: ${external}`);
}
const filterSummary = filterInfo.length > 0 ? `\n${filterInfo.join('\n')}` : '';
return `${summary}${currentDomain}${filterSummary}\n\n${linksList}\n\n${JSON.stringify(metadata, null, 2)}`;
}
function formatTabCreateResult(result, metadata) {
// Handle batch operations
if (result.batch_operation) {
const { summary, created_tabs, settings_used, warnings, errors } = result;
let output = `š Batch tab creation completed
š Summary: ${summary.successful}/${summary.total_requested} tabs created successfully
ā±ļø Execution time: ${summary.execution_time_ms}ms
š¦ Chunks processed: ${summary.chunks_processed}
`;
// Add warnings if any
if (warnings && warnings.length > 0) {
output += `ā ļø Warnings:\n${warnings.map(w => ` ⢠${w}`).join('\n')}\n\n`;
}
// Add created tabs info
if (created_tabs && created_tabs.length > 0) {
output += `ā
Created tabs:\n`;
created_tabs.forEach((tab, index) => {
output += ` ${index + 1}. ${tab.title || 'New Tab'} (ID: ${tab.tab_id})\n`;
output += ` š ${tab.actual_url || tab.url}\n`;
if (tab.active) output += ` šÆ Active tab\n`;
});
output += '\n';
}
// Add errors if any
if (errors && errors.length > 0) {
output += `ā Errors:\n`;
errors.forEach((error, index) => {
output += ` ${index + 1}. ${error.url}: ${error.error}\n`;
});
output += '\n';
}
// Add settings used (if available)
if (settings_used) {
output += `āļø Settings used:\n`;
output += ` ⢠Chunk size: ${settings_used.chunk_size}\n`;
output += ` ⢠Delay between chunks: ${settings_used.delay_between_chunks}ms\n`;
output += ` ⢠Delay between tabs: ${settings_used.delay_between_tabs}ms\n`;
}
return output + `\n${JSON.stringify(metadata, null, 2)}`;
}
// Handle single tab operations (existing logic)
if (result.success) {
return `ā
New tab created successfully
š Tab ID: ${result.tab_id}
š URL: ${result.url || 'about:blank'}
šÆ Active: ${result.active ? 'Yes' : 'No'}
š Title: ${result.title || 'New Tab'}
${result.warning ? `ā ļø Warning: ${result.warning}` : ''}
${JSON.stringify(metadata, null, 2)}`;
} else {
return `ā Failed to create tab: ${result.error || 'Unknown error'}
${JSON.stringify(metadata, null, 2)}`;
}
}
function formatTabCloseResult(result, metadata) {
if (result.success) {
const tabText = result.count === 1 ? 'tab' : 'tabs';
return `ā
Successfully closed ${result.count} ${tabText}
š Closed tab IDs: ${result.closed_tabs.join(', ')}
${JSON.stringify(metadata, null, 2)}`;
} else {
return `ā Failed to close tabs: ${result.error || 'Unknown error'}
${JSON.stringify(metadata, null, 2)}`;
}
}
function formatTabListResult(result, metadata) {
if (!result.success || !result.tabs || result.tabs.length === 0) {
return `š No tabs found
${JSON.stringify(metadata, null, 2)}`;
}
const summary = `š Found ${result.count} open tabs:
šÆ Active tab: ${result.active_tab || 'None'}
`;
const tabsList = result.tabs.map((tab, index) => {
const activeIcon = tab.active ? 'š¢' : 'āŖ';
const statusInfo = tab.status ? ` [${tab.status}]` : '';
const pinnedInfo = tab.pinned ? ' š' : '';
return `${index + 1}. ${activeIcon} **${tab.title}**${pinnedInfo}${statusInfo}
š ID: ${tab.id} | š ${tab.url}`;
}).join('\n\n');
return `${summary}${tabsList}
${JSON.stringify(metadata, null, 2)}`;
}
function formatTabSwitchResult(result, metadata) {
if (result.success) {
return `ā
Successfully switched to tab
š Tab ID: ${result.tab_id}
š Title: ${result.title}
š URL: ${result.url}
š Window ID: ${result.window_id}
${JSON.stringify(metadata, null, 2)}`;
} else {
return `ā Failed to switch tabs: ${result.error || 'Unknown error'}
${JSON.stringify(metadata, null, 2)}`;
}
}
function formatElementStateResult(result, metadata) {
const element = result.element_name || result.element_id || 'Unknown element';
const state = result.state || {};
let summary = `š Element State: ${element}
š **Interaction Readiness**: ${state.interaction_ready ? 'ā
Ready' : 'ā Not Ready'}
**Detailed State:**
⢠Disabled: ${state.disabled ? 'ā Yes' : 'ā
No'}
⢠Visible: ${state.visible ? 'ā
Yes' : 'ā No'}
⢠Clickable: ${state.clickable ? 'ā
Yes' : 'ā No'}
⢠Focusable: ${state.focusable ? 'ā
Yes' : 'ā No'}
⢠Has Text: ${state.hasText ? 'ā
Yes' : 'ā No'}
⢠Is Empty: ${state.isEmpty ? 'ā Yes' : 'ā
No'}`;
if (result.current_value) {
summary += `
š **Current Value**: "${result.current_value}"`;
}
return `${summary}
${JSON.stringify(metadata, null, 2)}`;
}
function formatPageStyleResult(result, metadata) {
const successIcon = result.success ? 'ā
' : 'ā';
const statusText = result.success ? 'successfully applied' : 'failed to apply';
let summary = `šØ Page styling ${statusText}\n\n`;
summary += `š **Operation Details:**\n`;
summary += `⢠**Mode:** ${result.mode || 'Unknown'}\n`;
if (result.theme) {
summary += `⢠**Theme:** ${result.theme}\n`;
}
if (result.applied_css !== undefined) {
summary += `⢠**CSS Applied:** ${result.applied_css} characters\n`;
}
if (result.description) {
summary += `⢠**Result:** ${result.description}\n`;
}
if (result.remember_enabled) {
summary += `⢠**Saved:** Style preferences saved for this domain\n`;
}
if (result.effect_duration) {
summary += `⢠**Effect Duration:** ${result.effect_duration} seconds\n`;
}
if (result.mood) {
summary += `⢠**Mood Applied:** "${result.mood}"\n`;
}
if (result.intensity) {
summary += `⢠**Intensity:** ${result.intensity}\n`;
}
if (!result.success && result.error) {
summary += `\nā **Error:** ${result.error}\n`;
}
if (result.warning) {
summary += `\nā ļø **Warning:** ${result.warning}\n`;
}
summary += `\nš” **Tip:** Use mode="reset" to restore original page styling`;
return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`;
}
// Enhanced fallback tools when extension is not connected
function getFallbackTools() {
return [
{
name: "page_analyze",
description:
"š BACKGROUND TAB READY: Analyze any tab without switching! Two-phase intelligent page analysis with token efficiency optimization. Use tab_id parameter to analyze background tabs while staying on current page. (Extension required)",
inputSchema: {
type: "object",
properties: {
intent_hint: {
type: "string",
description:
"What user wants to do: post_tweet, search, login, etc.",
},
phase: {
type: "string",
enum: ["discover", "detailed"],
default: "discover",
description:
"Analysis phase: 'discover' for quick scan, 'detailed' for full analysis",
},
},
required: ["intent_hint"],
},
},
{
name: "page_extract_content",
description:
"š BACKGROUND TAB READY: Extract content from any tab without switching! Perfect for analyzing multiple research tabs, articles, or pages simultaneously. Use tab_id to target specific background tabs. (Extension required)",
inputSchema: {
type: "object",
properties: {
content_type: {
type: "string",
enum: ["article", "search_results", "posts"],
description: "Type of content to extract",
},
summarize: {
type: "boolean",
default: true,
description:
"Return summary instead of full content (saves tokens)",
},
},
required: ["content_type"],
},
},
{
name: "element_click",
description:
"š±ļø BACKGROUND TAB READY: Click elements in any tab without switching! Perform actions on background tabs while staying on current page. Use tab_id to target specific tabs. (Extension required)",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Element ID from page_analyze",
},
click_type: {
type: "string",
enum: ["left", "right", "double"],
default: "left",
},
},
required: ["element_id"],
},
},
{
name: "element_fill",
description:
"āļø BACKGROUND TAB READY: Fill forms in any tab without switching! Enhanced focus and event simulation for modern web apps with anti-detection bypass for Twitter/X, LinkedIn, Facebook. Use tab_id to fill forms in background tabs. (Extension required)",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Element ID from page_analyze",
},
value: {
type: "string",
description: "Text to input",
},
clear_first: {
type: "boolean",
default: true,
description: "Clear existing content before filling",
},
},
required: ["element_id", "value"],
},
},
{
name: "page_navigate",
description:
"š§ Navigate to URLs with wait conditions (Extension required)",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to navigate to" },
wait_for: {
type: "string",
description: "CSS selector to wait for after navigation",
},
},
required: ["url"],
},
},
{
name: "page_wait_for",
description: "ā³ Wait for elements or conditions (Extension required)",
inputSchema: {
type: "object",
properties: {
condition_type: {
type: "string",
enum: ["element_visible", "text_present"],
description: "Type of condition to wait for",
},
selector: {
type: "string",
description: "CSS selector (for element_visible condition)",
},
text: {
type: "string",
description: "Text to wait for (for text_present condition)",
},
},
required: ["condition_type"],
},
},
// Tab Management Tools
{
name: "tab_create",
description: "Creates tabs. CRITICAL: For multiple identical tabs, ALWAYS use 'count' parameter! Examples: {url: 'https://x.com', count: 5} creates 5 Twitter tabs. {url: 'https://github.com', count: 10} creates 10 GitHub tabs. Single tab: {url: 'https://example.com'}. Multiple different URLs: {urls: ['url1', 'url2']}.",
inputSchema: {
type: "object",
examples: [
{ url: "https://x.com", count: 5 }, // CORRECT: Creates 5 identical Twitter tabs in one batch
{ url: "https://github.com", count: 10 }, // CORRECT: Creates 10 GitHub tabs
{ urls: ["https://x.com/post1", "https://x.com/post2", "https://google.com"] }, // CORRECT: Different URLs in batch
{ url: "https://example.com" } // Single tab only
],
properties: {
url: {
type: "string",
description: "Single URL to open. Can be used with 'count' to create multiple identical tabs"
},
urls: {
type: "array",
items: { type: "string" },
description: "PREFERRED FOR MULTIPLE URLS: Array of URLs to open ALL AT ONCE in a single batch operation. Pass ALL URLs here instead of making multiple calls! Example: ['https://x.com/post1', 'https://x.com/post2', 'https://google.com']",
maxItems: 100
},
count: {
type: "number",
default: 1,
minimum: 1,
maximum: 50,
description: "REQUIRED FOR MULTIPLE IDENTICAL TABS: Set this to N to create N copies of the same URL. For '5 Twitter tabs' use count=5 with url='https://x.com'. DO NOT make 5 separate calls!"
},
active: {
type: "boolean",
default: true,
description: "Whether to activate the last created tab (single tab only)"
},
wait_for: {
type: "string",
description: "CSS selector to wait for after tab creation (single tab only)"
},
timeout: {
type: "number",
default: 10000,
description: "Maximum wait time per tab in milliseconds"
},
batch_settings: {
type: "object",
description: "Performance control settings for batch operations",
properties: {
chunk_size: {
type: "number",
default: 5,
minimum: 1,
maximum: 10,
description: "Number of tabs to create per batch"
},
delay_between_chunks: {
type: "number",
default: 1000,
minimum: 100,
maximum: 5000,
description: "Delay between batches in milliseconds"
},
delay_between_tabs: {
type: "number",
default: 200,
minimum: 50,
maximum: 1000,
description: "Delay between individual tabs in milliseconds"
}
}
}
}
}
},
{
name: "tab_close",
description: "ā Close specific tab(s) by ID or close current tab (Extension required)",
inputSchema: {
type: "object",
properties: {
tab_id: {
type: "number",
description: "Specific tab ID to close (optional, closes current tab if not provided)"
},
tab_ids: {
type: "array",
items: { type: "number" },
description: "Array of tab IDs to close multiple tabs"
}
}
}
},
{
name: "tab_list",
description: "š TAB DISCOVERY: Get list of all open tabs with IDs for background tab targeting! Shows content script readiness status and tab details. Essential for multi-tab workflows - use tab IDs with other tools to work on background tabs. (Extension required)",
inputSchema: {
type: "object",
properties: {
current_window_only: {
type: "boolean",
default: true,
description: "Only return tabs from the current window"
},
include_details: {
type: "boolean",
default: true,
description: "Include additional tab details (title, favicon, etc.)"
}
}
}
},
{
name: "tab_switch",
description: "š Switch to a specific tab by ID (Extension required)",
inputSchema: {
type: "object",
properties: {
tab_id: {
type: "number",
description: "Tab ID to switch to"
}
},
required: ["tab_id"]
}
},
// Element State Tools
{
name: "element_get_state",
description: "š Get detailed state information for a specific element (disabled, clickable, etc.) (Extension required)",
inputSchema: {
type: "object",
properties: {
element_id: {
type: "string",
description: "Element ID from page_analyze"
}
},
required: ["element_id"]
}
},
// Workspace and Reference Management Tools
{
name: "get_bookmarks",
description: "Get all bookmarks or search for specific bookmarks (Extension required)",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query for bookmarks (optional)"
}
}
}
},
{
name: "add_bookmark",
description: "Add a new bookmark (Extension required)",
inputSchema: {
type: "object",
properties: {
title: {
type: "string",
description: "Title of the bookmark"
},
url: {
type: "string",
description: "URL of the bookmark"
},
parentId: {
type: "string",
description: "ID of the parent folder (optional)"
}
},
required: ["title", "url"]
}
},
{
name: "get_history",
description: "š Search browser history with comprehensive filters for finding previous work (Extension required)",
inputSchema: {
type: "object",
properties: {
keywords: {
type: "string",
description: "Search keywords to match in page titles and URLs"
},
start_date: {
type: "string",
format: "date-time",
description: "Start date for history search (ISO 8601 format)"
},
end_date: {
type: "string",
format: "date-time",
description: "End date for history search (ISO 8601 format)"
},
domains: {
type: "array",
items: { type: "string" },
description: "Filter by specific domains"
},
min_visit_count: {
type: "number",
default: 1,
description: "Minimum visit count threshold"
},
max_results: {
type: "number",
default: 50,
maximum: 500,
description: "Maximum number of results to return"
},
sort_by: {
type: "string",
enum: ["visit_time", "visit_count", "title"],
default: "visit_time",
description: "Sort results by visit time, visit count, or title"
},
sort_order: {
type: "string",
enum: ["desc", "asc"],
default: "desc",
description: "Sort order"
}
}
}
},
{
name: "get_selected_text",
description: "š BACKGROUND TAB READY: Get selected text from any tab without switching! Perfect for collecting quotes, citations, or highlighted content from multiple research tabs simultaneously. (Extension required)",
inputSchema: {
type: "object",
properties: {
include_metadata: {
type: "boolean",
default: true,
description: "Include metadata about the selection (element info, position, etc.)"
},
max_length: {
type: "number",
default: 10000,
description: "Maximum length of text to return"
}
}
}
},
{
name: "page_scroll",
description: "š BACKGROUND TAB READY: Scroll any tab without switching! Critical for long pages. Navigate through content in background tabs while staying on current page. Use tab_id to target specific tabs. (Extension required)",
inputSchema: {
type: "object",
properties: {
direction: {
type: "string",
enum: ["up", "down", "left", "right", "top", "bottom"],
default: "down",
description: "Direction to scroll"
},
amount: {
type: "string",
enum: ["small", "medium", "large", "page", "custom"],
default: "medium",
description: "Amount to scroll"
},
pixels: {
type: "number",
description: "Custom pixel amount (when amount is 'custom')"
},
smooth: {
type: "boolean",
default: true,
description: "Use smooth scrolling animation"
},
element_id: {
type: "string",
description: "Scroll to specific element (overrides direction/amount)"
},
wait_after: {
type: "number",
default: 500,
description: "Milliseconds to wait after scrolling"
}
}
}
},
{
name: "get_page_links",
description: "š Get all hyperlinks on the current page with smart filtering (Extension required)",
inputSchema: {
type: "object",
properties: {
include_internal: {
type: "boolean",
default: true,
description: "Include internal links (same domain)"
},
include_external: {
type: "boolean",
default: true,
description: "Include external links (different domains)"
},
domain_filter: {
type: "string",
description: "Filter links to include only specific domain(s)"
},
max_results: {
type: "number",
default: 100,
maximum: 500,
description: "Maximum number of links to return"
}
}
}
},
{
name: "page_style",
description: "šØ Transform page appearance with themes, colors, fonts, and fun effects! Apply preset themes like 'dark_hacker', 'retro_80s', or create custom styles. Perfect for making boring pages fun or improving readability.",
inputSchema: {
type: "object",
examples: [
{ mode: "preset", theme: "dark_hacker" },
{ mode: "custom", background: "#000", text_color: "#00ff00", font: "monospace" },
{ mode: "ai_mood", mood: "cozy coffee shop vibes", intensity: "strong" },
{ mode: "effect", effect: "matrix_rain", duration: 30 }
],
properties: {
mode: {
type: "string",
enum: ["preset", "custom", "ai_mood", "effect", "reset"],
description: "Styling mode to use"
},
theme: {
type: "string",
enum: ["dark_hacker", "retro_80s", "rainbow_party", "minimalist_zen", "high_contrast", "cyberpunk", "pastel_dream", "newspaper"],
description: "Preset theme name (when mode=preset)"
},
background: {
type: "string",
description: "Background color/gradient"
},
text_color: {
type: "string",
description: "Text color"
},
font: {
type: "string",
description: "Font family"
},
font_size: {
type: "string",
description: "Font size (e.g., '1.2em', '16px')"
},
mood: {
type: "string",
description: "Describe desired mood/feeling (when mode=ai_mood)"
},
intensity: {
type: "string",
enum: ["subtle", "medium", "strong"],
default: "medium"
},
effect: {
type: "string",
enum: ["matrix_rain", "floating_particles", "cursor_trail