diff --git a/README.md b/README.md index f567d05..dac8dc2 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ OpenDia is an open alternative to Dia. Connect to your browser with MCP & do any } ``` -## Enhanced MCP Tools (8 Total) +## Enhanced MCP Tools (11 Total) -### šŸŽÆ Core Automation Tools (6 Tools) +### šŸŽÆ Core Automation Tools (7 Tools) - **page_analyze**: Intelligent page analysis using pattern database + semantic analysis - Finds relevant elements based on user intent (e.g., "post_tweet", "search", "login") @@ -67,10 +67,10 @@ OpenDia is an open alternative to Dia. Connect to your browser with MCP & do any - Supports different click types (left, right, double) - Auto-scrolls elements into view -- **element_fill**: Smart form filling - - Fill input fields and textareas +- **element_fill**: Smart form filling with anti-detection bypass + - Fill input fields and textareas with specialized bypasses for Twitter/X, LinkedIn, Facebook - Supports contenteditable elements - - Option to clear existing content first + - Uses platform-specific techniques to avoid bot detection - **page_navigate**: Enhanced navigation - Navigate to URLs with optional wait conditions @@ -82,10 +82,39 @@ OpenDia is an open alternative to Dia. Connect to your browser with MCP & do any - Wait for specific text to appear on page - Configurable timeout periods -### šŸ”§ Essential Legacy Tools (2 Tools) +- **browser_navigate**: URL navigation + - Navigate to URLs in the active tab + - Simple navigation tool for compatibility -- **browser_navigate**: Legacy navigation (compatibility) -- **browser_execute_script**: CSP-aware JavaScript execution with fallbacks +### šŸ“‘ Tab Management Tools (4 Tools) + +- **tab_create**: Create new tabs with advanced options + - Create tabs with or without URLs + - Control tab activation and focus + - Wait for elements to load after creation + - Perfect for multi-tab workflows + +- **tab_close**: Close tabs with flexible targeting + - Close current tab, specific tab by ID, or multiple tabs + - Batch close operations for cleanup + - Safe handling of tab closure + +- **tab_list**: Get comprehensive tab information + - List all open tabs with details (title, URL, status) + - Filter by current window or all windows + - Track active tab and tab states + +- **tab_switch**: Switch between tabs intelligently + - Switch to specific tabs by ID + - Focus windows automatically + - Essential for multi-tab automation workflows + +### šŸ”§ State Management Tools (1 Tool) + +- **element_get_state**: Get detailed state information for elements + - Check if elements are disabled, clickable, visible + - Get current values and element properties + - Essential for conditional automation logic ## šŸš€ Key Features diff --git a/opendia-extension/background.js b/opendia-extension/background.js index 2edeebd..6fc721f 100644 --- a/opendia-extension/background.js +++ b/opendia-extension/background.js @@ -213,11 +213,9 @@ function getAvailableTools() { required: ["condition_type"] } }, - - // Essential legacy tools for compatibility { name: "browser_navigate", - description: "Navigate to a URL in the active tab (legacy)", + description: "Navigate to a URL in the active tab", inputSchema: { type: "object", properties: { @@ -226,16 +224,85 @@ function getAvailableTools() { required: ["url"], }, }, + + // Tab Management Tools { - name: "browser_execute_script", - description: "Execute JavaScript in the active tab", + name: "tab_create", + description: "Create a new tab with optional URL and activation", inputSchema: { type: "object", properties: { - code: { type: "string", description: "JavaScript code to execute" }, + url: { + type: "string", + description: "URL to open in the new tab (optional)" + }, + active: { + type: "boolean", + default: true, + description: "Whether to activate the new tab" + }, + wait_for: { + type: "string", + description: "CSS selector to wait for after tab creation (if URL provided)" + }, + timeout: { + type: "number", + default: 10000, + description: "Maximum wait time in milliseconds" + } + } + } + }, + { + name: "tab_close", + description: "Close specific tab(s) by ID or close current tab", + 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: "Get list of all open tabs with their details", + 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", + inputSchema: { + type: "object", + properties: { + tab_id: { + type: "number", + description: "Tab ID to switch to" + } }, - required: ["code"], - }, + required: ["tab_id"] + } }, // Element State Tools @@ -253,24 +320,175 @@ function getAvailableTools() { required: ["element_id"] } }, - - // Analytics and Performance Tools + // Workspace and Reference Management Tools { - name: "get_analytics", - description: "Get token usage analytics and performance metrics", + name: "get_bookmarks", + description: "Get all bookmarks or search for specific bookmarks", inputSchema: { type: "object", - properties: {}, - additionalProperties: false + properties: { + query: { + type: "string", + description: "Search query for bookmarks (optional)" + } + } } }, { - name: "clear_analytics", - description: "Clear all analytics data and reset performance tracking", + name: "add_bookmark", + description: "Add a new bookmark", inputSchema: { type: "object", - properties: {}, - additionalProperties: false + 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 by date/keywords", + 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 (e.g., ['github.com', 'stackoverflow.com'])" + }, + 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 (descending or ascending)" + } + } + } + }, + { + name: "get_selected_text", + description: "Get the currently selected text on the page", + 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: "Scroll the page in various directions and amounts - critical for long pages", + 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 filtering options", + inputSchema: { + type: "object", + properties: { + link_type: { + type: "string", + enum: ["all", "internal", "external"], + default: "all", + description: "Filter by internal/external links" + }, + domains: { + type: "array", + items: { type: "string" }, + description: "Filter by specific domains (optional)" + }, + max_results: { + type: "number", + default: 50, + maximum: 200, + description: "Maximum links to return" + } + } } }, ]; @@ -303,26 +521,46 @@ async function handleMCPRequest(message) { case "page_wait_for": result = await sendToContentScript('wait_for', params); break; - - // Essential legacy tools for compatibility case "browser_navigate": result = await navigateToUrl(params.url); break; - case "browser_execute_script": - result = await executeScript(params.code); + + // Tab management tools + case "tab_create": + result = await createTab(params); + break; + case "tab_close": + result = await closeTabs(params); + break; + case "tab_list": + result = await listTabs(params); + break; + case "tab_switch": + result = await switchToTab(params.tab_id); break; // Element state tools case "element_get_state": result = await sendToContentScript('get_element_state', params); break; - - // Analytics tools - case "get_analytics": - result = await sendToContentScript('get_analytics', {}); + // Workspace and Reference Management Tools + case "get_bookmarks": + result = await getBookmarks(params); break; - case "clear_analytics": - result = await sendToContentScript('clear_analytics', {}); + case "add_bookmark": + result = await addBookmark(params); + break; + case "get_history": + result = await getHistory(params); + break; + case "get_selected_text": + result = await getSelectedText(params); + break; + case "page_scroll": + result = await sendToContentScript('page_scroll', params); + break; + case "get_page_links": + result = await sendToContentScript('get_page_links', params); break; default: throw new Error(`Unknown method: ${method}`); @@ -364,10 +602,10 @@ async function sendToContentScript(action, data) { chrome.tabs.sendMessage(activeTab.id, { action, data }, (response) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); - } else if (response.success) { + } else if (response && response.success) { resolve(response.data); } else { - reject(new Error(response.error || 'Unknown error')); + reject(new Error(response?.error || 'Unknown error')); } }); }); @@ -421,58 +659,473 @@ async function waitForElement(tabId, selector, timeout = 5000) { throw new Error(`Timeout waiting for element: ${selector}`); } -async function executeScript(code) { - const [activeTab] = await chrome.tabs.query({ - active: true, - currentWindow: true, +// Tab Management Functions +async function createTab(params) { + const { url, active = true, wait_for, timeout = 10000 } = params; + + const createProperties = { active }; + if (url) { + createProperties.url = url; + } + + const newTab = await chrome.tabs.create(createProperties); + + // If URL was provided and wait_for is specified, wait for the element + if (url && wait_for) { + try { + await waitForElement(newTab.id, wait_for, timeout); + } catch (error) { + return { + success: true, + tab_id: newTab.id, + url: newTab.url, + warning: `Tab created but wait condition failed: ${error.message}` + }; + } + } + + return { + success: true, + tab_id: newTab.id, + url: newTab.url || 'about:blank', + active: newTab.active, + title: newTab.title || 'New Tab' + }; +} + +async function closeTabs(params) { + const { tab_id, tab_ids } = params; + + let tabsToClose = []; + + if (tab_ids && Array.isArray(tab_ids)) { + // Close multiple tabs + tabsToClose = tab_ids; + } else if (tab_id) { + // Close specific tab + tabsToClose = [tab_id]; + } else { + // Close current tab + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + if (activeTab) { + tabsToClose = [activeTab.id]; + } + } + + if (tabsToClose.length === 0) { + throw new Error('No tabs specified to close'); + } + + // Close tabs + await chrome.tabs.remove(tabsToClose); + + return { + success: true, + closed_tabs: tabsToClose, + count: tabsToClose.length + }; +} + +async function listTabs(params) { + const { current_window_only = true, include_details = true } = params; + + const queryOptions = {}; + if (current_window_only) { + queryOptions.currentWindow = true; + } + + const tabs = await chrome.tabs.query(queryOptions); + + const tabList = tabs.map(tab => { + const basicInfo = { + id: tab.id, + url: tab.url, + active: tab.active, + title: tab.title + }; + + if (include_details) { + return { + ...basicInfo, + index: tab.index, + pinned: tab.pinned, + status: tab.status, + favIconUrl: tab.favIconUrl, + windowId: tab.windowId, + incognito: tab.incognito + }; + } + + return basicInfo; }); + return { + success: true, + tabs: tabList, + count: tabList.length, + active_tab: tabs.find(tab => tab.active)?.id || null + }; +} + +async function switchToTab(tabId) { + // First, get tab info to ensure it exists + const tab = await chrome.tabs.get(tabId); + + if (!tab) { + throw new Error(`Tab with ID ${tabId} not found`); + } + + // Switch to the tab + await chrome.tabs.update(tabId, { active: true }); + + // Also focus the window containing the tab + await chrome.windows.update(tab.windowId, { focused: true }); + + return { + success: true, + tab_id: tabId, + url: tab.url, + title: tab.title, + window_id: tab.windowId + }; +} + +// Workspace and Reference Management Functions +async function getBookmarks(params) { + const { query } = params; + + let bookmarks; + if (query) { + bookmarks = await chrome.bookmarks.search(query); + } else { + bookmarks = await chrome.bookmarks.getTree(); + } + + return { + success: true, + bookmarks, + count: bookmarks.length + }; +} + +async function addBookmark(params) { + const { title, url, parentId } = params; + + const bookmark = await chrome.bookmarks.create({ + title, + url, + parentId + }); + + return { + success: true, + bookmark + }; +} + +// History Management Function +async function getHistory(params) { + const { + keywords = "", + start_date, + end_date, + domains = [], + min_visit_count = 1, + max_results = 50, + sort_by = "visit_time", + sort_order = "desc" + } = params; + try { - // Use chrome.scripting.executeScript with a function instead of eval - const results = await chrome.scripting.executeScript({ - target: { tabId: activeTab.id }, - func: (codeToExecute) => { - // Execute code safely without eval + // Chrome History API search configuration + const searchQuery = { + text: keywords, + maxResults: Math.min(max_results * 3, 1000), // Over-fetch for filtering + }; + + // Add date range if specified + if (start_date) { + searchQuery.startTime = new Date(start_date).getTime(); + } + if (end_date) { + searchQuery.endTime = new Date(end_date).getTime(); + } + + // Execute history search + const historyItems = await chrome.history.search(searchQuery); + + // Apply advanced filters + let filteredItems = historyItems.filter(item => { + // Domain filter + if (domains.length > 0) { try { - // Create a script element to bypass CSP - const script = document.createElement('script'); - script.textContent = codeToExecute; - document.head.appendChild(script); - document.head.removeChild(script); - return { success: true, executed: true }; - } catch (error) { - // Fallback: try to execute common operations directly - if (codeToExecute.includes('window.scrollTo')) { - const match = codeToExecute.match(/window\.scrollTo\((\d+),\s*(\d+|[^)]+)\)/); - if (match) { - const x = parseInt(match[1]); - const y = match[2].includes('document.body.scrollHeight') ? - document.body.scrollHeight / 2 : parseInt(match[2]); - window.scrollTo(x, y); - return { success: true, scrolled: true, x, y }; - } + const itemDomain = new URL(item.url).hostname; + if (!domains.some(domain => itemDomain.includes(domain))) { + return false; } - - if (codeToExecute.includes('document.querySelector')) { - // Handle simple querySelector operations - const match = codeToExecute.match(/document\.querySelector\(['"]([^'"]+)['"]\)/); - if (match) { - const element = document.querySelector(match[1]); - return { success: true, element: element ? 'found' : 'not found', selector: match[1] }; - } - } - - throw error; + } catch (e) { + // Skip items with invalid URLs + return false; } - }, - args: [code], + } + + // Visit count filter + if (item.visitCount < min_visit_count) { + return false; + } + + return true; }); - return results[0].result; + + // Sort results + filteredItems.sort((a, b) => { + let aVal, bVal; + switch (sort_by) { + case "visit_count": + aVal = a.visitCount; + bVal = b.visitCount; + break; + case "title": + aVal = (a.title || "").toLowerCase(); + bVal = (b.title || "").toLowerCase(); + break; + default: // visit_time + aVal = a.lastVisitTime; + bVal = b.lastVisitTime; + } + + if (sort_order === "asc") { + return aVal > bVal ? 1 : -1; + } else { + return aVal < bVal ? 1 : -1; + } + }); + + // Limit results + const results = filteredItems.slice(0, max_results); + + // Format response with comprehensive metadata + return { + success: true, + history_items: results.map(item => { + let domain; + try { + domain = new URL(item.url).hostname; + } catch (e) { + domain = "invalid-url"; + } + + return { + id: item.id, + url: item.url, + title: item.title || "Untitled", + last_visit_time: new Date(item.lastVisitTime).toISOString(), + visit_count: item.visitCount, + domain: domain, + typed_count: item.typedCount || 0 + }; + }), + metadata: { + total_found: filteredItems.length, + returned_count: results.length, + search_params: { + keywords: keywords || null, + date_range: start_date && end_date ? + `${start_date} to ${end_date}` : + start_date ? `from ${start_date}` : + end_date ? `until ${end_date}` : null, + domains: domains.length > 0 ? domains : null, + min_visit_count, + sort_by, + sort_order + }, + execution_time: new Date().toISOString(), + over_fetched: historyItems.length, + filters_applied: { + domain_filter: domains.length > 0, + visit_count_filter: min_visit_count > 1, + date_filter: !!(start_date || end_date), + keyword_filter: !!keywords + } + } + }; + } catch (error) { return { success: false, - error: error.message, - note: "CSP restrictions prevent arbitrary JavaScript execution. Try using specific automation tools instead." + error: `History search failed: ${error.message}`, + history_items: [], + metadata: { + total_found: 0, + returned_count: 0, + search_params: params, + execution_time: new Date().toISOString() + } + }; + } +} + +// Selected Text Management Function +async function getSelectedText(params) { + const { + include_metadata = true, + max_length = 10000 + } = params; + + try { + // Get the active tab + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + + if (!activeTab) { + return { + success: false, + error: "No active tab found", + selected_text: "", + metadata: { + execution_time: new Date().toISOString() + } + }; + } + + // Execute script to get selected text + const results = await chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + func: () => { + const selection = window.getSelection(); + const selectedText = selection.toString(); + + if (!selectedText) { + return { + text: "", + hasSelection: false, + metadata: null + }; + } + + // Get metadata about the selection + const range = selection.getRangeAt(0); + const rect = range.getBoundingClientRect(); + + // Get parent element info + const commonAncestor = range.commonAncestorContainer; + const parentElement = commonAncestor.nodeType === Node.TEXT_NODE + ? commonAncestor.parentElement + : commonAncestor; + + const metadata = { + length: selectedText.length, + word_count: selectedText.trim().split(/\s+/).filter(word => word.length > 0).length, + line_count: selectedText.split('\n').length, + position: { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height + }, + parent_element: { + tag_name: parentElement.tagName?.toLowerCase(), + class_name: parentElement.className, + id: parentElement.id, + text_content_length: parentElement.textContent?.length || 0 + }, + page_info: { + url: window.location.href, + title: document.title, + domain: window.location.hostname + }, + selection_info: { + anchor_offset: selection.anchorOffset, + focus_offset: selection.focusOffset, + range_count: selection.rangeCount, + is_collapsed: selection.isCollapsed + } + }; + + return { + text: selectedText, + hasSelection: true, + metadata: metadata + }; + } + }); + + const result = results[0]?.result; + + if (!result) { + return { + success: false, + error: "Failed to execute selection script", + selected_text: "", + metadata: { + execution_time: new Date().toISOString() + } + }; + } + + if (!result.hasSelection) { + return { + success: true, + selected_text: "", + has_selection: false, + message: "No text is currently selected on the page", + metadata: { + execution_time: new Date().toISOString(), + tab_info: { + id: activeTab.id, + url: activeTab.url, + title: activeTab.title + } + } + }; + } + + // Truncate text if it exceeds max_length + let selectedText = result.text; + let truncated = false; + if (selectedText.length > max_length) { + selectedText = selectedText.substring(0, max_length); + truncated = true; + } + + const response = { + success: true, + selected_text: selectedText, + has_selection: true, + character_count: result.text.length, + truncated: truncated, + metadata: { + execution_time: new Date().toISOString(), + tab_info: { + id: activeTab.id, + url: activeTab.url, + title: activeTab.title + } + } + }; + + // Include detailed metadata if requested + if (include_metadata && result.metadata) { + response.selection_metadata = result.metadata; + } + + return response; + + } catch (error) { + return { + success: false, + error: `Failed to get selected text: ${error.message}`, + selected_text: "", + has_selection: false, + metadata: { + execution_time: new Date().toISOString(), + error_details: error.stack + } }; } } @@ -493,6 +1146,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse({ connected: mcpSocket && mcpSocket.readyState === WebSocket.OPEN, }); + } else if (request.action === "getToolCount") { + const tools = getAvailableTools(); + sendResponse({ + toolCount: tools.length, + tools: tools.map(t => t.name) + }); } else if (request.action === "reconnect") { connectToMCPServer(); sendResponse({ success: true }); @@ -503,4 +1162,4 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { sendResponse({ success: true }); } return true; // Keep the message channel open -}); \ No newline at end of file +}); diff --git a/opendia-extension/content.js b/opendia-extension/content.js index b6277e6..a7652ea 100644 --- a/opendia-extension/content.js +++ b/opendia-extension/content.js @@ -467,6 +467,12 @@ class BrowserAutomation { current_value: this.getElementValue(element), }; break; + case "get_page_links": + result = await this.getPageLinks(data); + break; + case "page_scroll": + result = await this.scrollPage(data); + break; default: throw new Error(`Unknown action: ${action}`); } @@ -2476,6 +2482,241 @@ class BrowserAutomation { const jsonString = JSON.stringify(result); return Math.ceil(jsonString.length / 4); // Rough estimate: 4 chars per token } + // Get all links on the page with filtering options + async getPageLinks(options = {}) { + const { + include_internal = true, + include_external = true, + domain_filter = null, + max_results = 100 + } = options; + + const links = Array.from(document.querySelectorAll('a[href]')); + const currentDomain = this.extractDomain(window.location.href); + const results = []; + + for (const link of links) { + if (results.length >= max_results) break; + + const href = link.href; + const linkDomain = this.extractDomain(href); + const isInternal = this.isSameDomain(currentDomain, linkDomain); + + // Apply internal/external filter + if (!include_internal && isInternal) continue; + if (!include_external && !isInternal) continue; + + // Apply domain filter + if (domain_filter && !linkDomain.includes(domain_filter)) continue; + + const linkText = link.textContent?.trim() || ''; + const linkTitle = link.title || ''; + + results.push({ + url: href, + text: linkText, + title: linkTitle, + type: isInternal ? 'internal' : 'external', + domain: linkDomain + }); + } + + return { + links: results, + total_found: links.length, + returned: results.length, + current_domain: currentDomain + }; + } + + // Check if two domains are the same (handles subdomains) + isSameDomain(domain1, domain2) { + if (!domain1 || !domain2) return false; + + // Remove www. prefix for comparison + const clean1 = domain1.replace(/^www\./, ''); + const clean2 = domain2.replace(/^www\./, ''); + + return clean1 === clean2; + } + + // Extract domain from URL + extractDomain(url) { + try { + return new URL(url).hostname; + } catch { + return ''; + } + } + + // Scroll page with comprehensive options + async scrollPage(options = {}) { + const { + direction = 'down', + amount = 'medium', + pixels = null, + smooth = true, + element_id = null, + wait_after = 500 + } = options; + + const startPosition = { + x: window.scrollX, + y: window.scrollY + }; + + try { + // If element_id is provided, scroll to that element + if (element_id) { + const element = this.getElementById(element_id); + if (!element) { + throw new Error(`Element not found: ${element_id}`); + } + + element.scrollIntoView({ + behavior: smooth ? 'smooth' : 'instant', + block: 'center', + inline: 'center' + }); + + await new Promise(resolve => setTimeout(resolve, wait_after)); + + return { + success: true, + previous_position: startPosition, + new_position: { x: window.scrollX, y: window.scrollY }, + method: 'scroll_to_element', + element_id: element_id, + element_name: this.getElementName(element) + }; + } + + // Calculate scroll amount based on amount parameter + let scrollAmount; + if (amount === 'custom' && pixels) { + scrollAmount = pixels; + } else { + switch (amount) { + case 'small': + scrollAmount = Math.min(200, window.innerHeight * 0.25); + break; + case 'medium': + scrollAmount = Math.min(500, window.innerHeight * 0.5); + break; + case 'large': + scrollAmount = Math.min(800, window.innerHeight * 0.8); + break; + case 'page': + scrollAmount = window.innerHeight * 0.9; // Slightly less than full page for overlap + break; + default: + scrollAmount = Math.min(500, window.innerHeight * 0.5); + } + } + + // Calculate scroll direction + let scrollX = 0; + let scrollY = 0; + + switch (direction) { + case 'up': + scrollY = -scrollAmount; + break; + case 'down': + scrollY = scrollAmount; + break; + case 'left': + scrollX = -scrollAmount; + break; + case 'right': + scrollX = scrollAmount; + break; + case 'top': + // Scroll to top of page + if (smooth) { + window.scrollTo({ top: 0, left: window.scrollX, behavior: 'smooth' }); + } else { + window.scrollTo(window.scrollX, 0); + } + await new Promise(resolve => setTimeout(resolve, wait_after)); + return { + success: true, + previous_position: startPosition, + new_position: { x: window.scrollX, y: window.scrollY }, + direction: direction, + method: 'scroll_to_top' + }; + case 'bottom': + // Scroll to bottom of page + const maxY = Math.max( + document.body.scrollHeight, + document.documentElement.scrollHeight + ) - window.innerHeight; + if (smooth) { + window.scrollTo({ top: maxY, left: window.scrollX, behavior: 'smooth' }); + } else { + window.scrollTo(window.scrollX, maxY); + } + await new Promise(resolve => setTimeout(resolve, wait_after)); + return { + success: true, + previous_position: startPosition, + new_position: { x: window.scrollX, y: window.scrollY }, + direction: direction, + method: 'scroll_to_bottom' + }; + default: + throw new Error(`Unknown scroll direction: ${direction}`); + } + + // Perform the scroll + if (smooth) { + window.scrollBy({ + left: scrollX, + top: scrollY, + behavior: 'smooth' + }); + } else { + window.scrollBy(scrollX, scrollY); + } + + // Wait for scroll to complete + await new Promise(resolve => setTimeout(resolve, wait_after)); + + const finalPosition = { + x: window.scrollX, + y: window.scrollY + }; + + const actualScrolled = { + x: finalPosition.x - startPosition.x, + y: finalPosition.y - startPosition.y + }; + + return { + success: true, + previous_position: startPosition, + new_position: finalPosition, + direction: direction, + amount: amount, + requested_pixels: scrollAmount, + actual_scrolled: actualScrolled, + total_distance: Math.sqrt(actualScrolled.x ** 2 + actualScrolled.y ** 2), + smooth: smooth, + wait_after: wait_after + }; + + } catch (error) { + return { + success: false, + error: error.message, + previous_position: startPosition, + new_position: { x: window.scrollX, y: window.scrollY }, + direction: direction, + amount: amount + }; + } + } } // Initialize the automation system diff --git a/opendia-extension/manifest.json b/opendia-extension/manifest.json index e3dd1df..f3b1106 100644 --- a/opendia-extension/manifest.json +++ b/opendia-extension/manifest.json @@ -9,7 +9,9 @@ "storage", "scripting", "webNavigation", - "notifications" + "notifications", + "bookmarks", + "history" ], "host_permissions": [ "" @@ -32,4 +34,4 @@ "ids": ["*"], "matches": ["http://localhost/*"] } -} \ No newline at end of file +} diff --git a/opendia-extension/popup.html b/opendia-extension/popup.html index 6b9e4bb..e578516 100644 --- a/opendia-extension/popup.html +++ b/opendia-extension/popup.html @@ -332,7 +332,7 @@
Available Tools - 8 + Loading...
Current Page diff --git a/opendia-extension/popup.js b/opendia-extension/popup.js index 549c3ec..dc36de0 100644 --- a/opendia-extension/popup.js +++ b/opendia-extension/popup.js @@ -11,9 +11,20 @@ let dataSizeInfo = document.getElementById("data-size"); let expandButton = document.getElementById("expand-results"); let jsonViewer = document.getElementById("json-viewer"); -// Get initial tool count and page info -const tools = 8; // 6 core automation + 2 essential legacy tools -toolCount.textContent = tools; +// Get dynamic tool count from background script +function updateToolCount() { + if (chrome.runtime?.id) { + chrome.runtime.sendMessage({ action: "getToolCount" }, (response) => { + if (!chrome.runtime.lastError && response?.toolCount) { + toolCount.textContent = response.toolCount; + addLog(`Updated tool count: ${response.toolCount}`, "info"); + } else { + // Fallback to calculating from background script + toolCount.textContent = "18"; // Expected total based on background script + } + }); + } +} // Check connection status and get page info function checkStatus() { @@ -27,6 +38,9 @@ function checkStatus() { } }); + // Update tool count + updateToolCount(); + // Get current page info chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { if (tabs[0]) { diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index 6f0598a..6214e9e 100644 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -195,6 +195,33 @@ function formatToolResult(toolName, result) { )}` ); + 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); + default: // Legacy tools or unknown tools return JSON.stringify(result, null, 2); @@ -354,6 +381,237 @@ function formatElementFillResult(result, metadata) { 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) { + 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)}`; +} + // Enhanced fallback tools when extension is not connected function getFallbackTools() { return [ @@ -514,6 +772,100 @@ function getFallbackTools() { required: ["url"], }, }, + // Tab Management Tools + { + name: "tab_create", + description: "šŸ†• Create a new tab with optional URL and activation (Extension required)", + inputSchema: { + type: "object", + properties: { + url: { + type: "string", + description: "URL to open in the new tab (optional)" + }, + active: { + type: "boolean", + default: true, + description: "Whether to activate the new tab" + }, + wait_for: { + type: "string", + description: "CSS selector to wait for after tab creation (if URL provided)" + }, + timeout: { + type: "number", + default: 10000, + description: "Maximum wait time 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: "šŸ“‹ Get list of all open tabs with their details (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"] + } + }, { name: "browser_execute_script", description: @@ -526,6 +878,180 @@ function getFallbackTools() { required: ["code"], }, }, + // 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: "šŸ“ Get the currently selected text on the page (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: "šŸ“œ Scroll the page in various directions - critical for long pages (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" + } + } + } + }, ]; }