more tools

This commit is contained in:
Aaron Elijah Mars
2025-06-27 15:01:01 +02:00
parent 76854cc8d0
commit 59b4274b83
7 changed files with 1558 additions and 87 deletions

View File

@@ -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

View File

@@ -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
});
});

View File

@@ -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

View File

@@ -9,7 +9,9 @@
"storage",
"scripting",
"webNavigation",
"notifications"
"notifications",
"bookmarks",
"history"
],
"host_permissions": [
"<all_urls>"
@@ -32,4 +34,4 @@
"ids": ["*"],
"matches": ["http://localhost/*"]
}
}
}

View File

@@ -332,7 +332,7 @@
</div>
<div class="info-row">
<span class="info-label">Available Tools</span>
<span class="info-value" id="toolCount">8</span>
<span class="info-value" id="toolCount">Loading...</span>
</div>
<div class="info-row">
<span class="info-label">Current Page</span>

View File

@@ -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]) {

View File

@@ -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"
}
}
}
},
];
}