diff --git a/.fgp/skill.json b/.fgp/skill.json new file mode 100644 index 0000000..9380fe7 --- /dev/null +++ b/.fgp/skill.json @@ -0,0 +1,232 @@ +{ + "$schema": "https://fgp.dev/schemas/skill.json", + "name": "browser-gateway", + "version": "1.0.0", + "description": "Fast, high-performance browser automation via Chrome DevTools Protocol", + "author": { + "name": "Wolfgang Schoenberger", + "email": "support@fgp.dev" + }, + "repository": "https://github.com/fast-gateway-protocol/browser", + "homepage": "https://fgp.dev", + "license": "MIT", + "keywords": ["browser", "automation", "chrome", "cdp", "playwright"], + "category": "browser-automation", + + "binary": { + "type": "rust", + "cargo_package": "fgp-browser", + "build_command": "cargo build --release", + "executable": "target/release/browser-gateway" + }, + + "distribution": { + "prebuilt": { + "darwin-arm64": "https://github.com/fast-gateway-protocol/browser/releases/download/v${VERSION}/browser-gateway-darwin-arm64", + "darwin-x64": "https://github.com/fast-gateway-protocol/browser/releases/download/v${VERSION}/browser-gateway-darwin-x64", + "linux-x64": "https://github.com/fast-gateway-protocol/browser/releases/download/v${VERSION}/browser-gateway-linux-x64" + }, + "homebrew": { + "tap": "fast-gateway-protocol/fgp", + "formula": "browser-gateway" + } + }, + + "daemon": { + "name": "browser", + "socket_path": "${FGP_HOME}/services/browser/daemon.sock", + "pid_file": "${FGP_HOME}/services/browser/daemon.pid", + "log_file": "${FGP_HOME}/services/browser/daemon.log", + "start_command": ["${SKILL_BIN}", "start"], + "stop_command": ["${SKILL_BIN}", "stop"], + "health_method": "health" + }, + + "methods": [ + { + "name": "health", + "description": "Check daemon health status", + "params": {} + }, + { + "name": "browser.open", + "description": "Navigate to a URL", + "params": { + "url": { "type": "string", "required": true, "description": "URL to navigate to" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.snapshot", + "description": "Get accessibility tree (ARIA snapshot)", + "params": { + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.screenshot", + "description": "Capture PNG screenshot", + "params": { + "path": { "type": "string", "required": true, "description": "Output file path" }, + "full_page": { "type": "boolean", "description": "Capture full scrollable page (default: false)" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.click", + "description": "Click an element by selector", + "params": { + "selector": { "type": "string", "required": true, "description": "CSS selector or ARIA ref" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.fill", + "description": "Fill an input field", + "params": { + "selector": { "type": "string", "required": true, "description": "CSS selector" }, + "value": { "type": "string", "required": true, "description": "Value to fill" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.press", + "description": "Press a key", + "params": { + "key": { "type": "string", "required": true, "description": "Key to press (e.g., Enter, Tab)" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.select", + "description": "Select a dropdown option", + "params": { + "selector": { "type": "string", "required": true, "description": "CSS selector" }, + "value": { "type": "string", "required": true, "description": "Option value" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.check", + "description": "Check/uncheck a checkbox", + "params": { + "selector": { "type": "string", "required": true, "description": "CSS selector" }, + "checked": { "type": "boolean", "description": "Check state (default: true)" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.hover", + "description": "Hover over an element", + "params": { + "selector": { "type": "string", "required": true, "description": "CSS selector" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.scroll", + "description": "Scroll the page or element", + "params": { + "direction": { "type": "string", "enum": ["up", "down", "left", "right"], "description": "Scroll direction (default: down)" }, + "amount": { "type": "integer", "description": "Pixels to scroll" }, + "selector": { "type": "string", "description": "Element to scroll (optional)" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.press_combo", + "description": "Press key with modifiers (e.g., Ctrl+A)", + "params": { + "key": { "type": "string", "required": true, "description": "Key to press" }, + "modifiers": { + "type": "array", + "items": { "type": "string", "enum": ["ctrl", "alt", "shift", "meta"] }, + "description": "Modifier keys" + }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.upload", + "description": "Upload a file to an input element", + "params": { + "selector": { "type": "string", "required": true, "description": "File input selector" }, + "path": { "type": "string", "required": true, "description": "Path to file to upload" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.session.new", + "description": "Create a new isolated browser session", + "params": { + "session_id": { "type": "string", "description": "Custom session ID (optional)" } + } + }, + { + "name": "browser.session.list", + "description": "List active browser sessions", + "params": {} + }, + { + "name": "browser.session.close", + "description": "Close a browser session", + "params": { + "session_id": { "type": "string", "required": true, "description": "Session ID to close" } + } + }, + { + "name": "browser.state.save", + "description": "Save browser state (cookies, localStorage)", + "params": { + "name": { "type": "string", "required": true, "description": "State name" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.state.load", + "description": "Load saved browser state", + "params": { + "name": { "type": "string", "required": true, "description": "State name" }, + "session_id": { "type": "string", "description": "Session ID (optional)" } + } + }, + { + "name": "browser.state.list", + "description": "List saved browser states", + "params": {} + } + ], + + "requirements": { + "chrome": { + "type": "browser", + "names": ["Google Chrome", "Chromium"], + "min_version": "90", + "install_hint": "brew install --cask google-chrome" + } + }, + + "exports": { + "mcp": { + "enabled": true, + "command": "${SKILL_BIN}", + "args": ["mcp"], + "tools_prefix": "fgp_browser" + }, + "claude": { + "enabled": true, + "skill_name": "browser-fgp", + "triggers": ["browser", "navigate", "screenshot", "click", "fill", "automation", "web page"] + }, + "cursor": { + "enabled": true, + "server_name": "fgp-browser" + }, + "windsurf": { + "enabled": false + }, + "continue_dev": { + "enabled": false + } + } +} diff --git a/src/browser/aria.rs b/src/browser/aria.rs index fdcc087..783c49e 100644 --- a/src/browser/aria.rs +++ b/src/browser/aria.rs @@ -48,7 +48,7 @@ fn is_interactive_node(node: &CdpAxNode) -> bool { .role .as_ref() .and_then(|role| role.value.as_ref()) - .map_or(false, |value| { + .is_some_and(|value| { let role_str = json_as_str(value).unwrap_or(""); matches!( role_str, @@ -96,7 +96,7 @@ fn is_focusable(node: &CdpAxNode) -> bool { && p.value .value .as_ref() - .and_then(|v| json_as_bool(v)) + .and_then(json_as_bool) .unwrap_or(false) }) }) @@ -267,7 +267,7 @@ fn convert_node_ref(node: &CdpAxNode, counter: &mut usize) -> AriaNode { && p.value .value .as_ref() - .and_then(|v| json_as_bool(v)) + .and_then(json_as_bool) .unwrap_or(false) }) }) @@ -282,7 +282,7 @@ fn convert_node_ref(node: &CdpAxNode, counter: &mut usize) -> AriaNode { && p.value .value .as_ref() - .and_then(|v| json_as_bool(v)) + .and_then(json_as_bool) .unwrap_or(false) }) }) diff --git a/src/browser/client.rs b/src/browser/client.rs index cbd0a89..3f39b0d 100644 --- a/src/browser/client.rs +++ b/src/browser/client.rs @@ -735,7 +735,7 @@ impl BrowserClient { }) .collect(); - headless_dirs.sort_by(|a, b| b.file_name().cmp(&a.file_name())); + headless_dirs.sort_by_key(|b| std::cmp::Reverse(b.file_name())); for dir in headless_dirs { let binary = dir.path().join(subdir_name).join("chrome-headless-shell"); diff --git a/src/service.rs b/src/service.rs index e06a8ed..ee7ce39 100644 --- a/src/service.rs +++ b/src/service.rs @@ -9,7 +9,7 @@ use fgp_daemon::service::MethodInfo; use fgp_daemon::FgpService; use serde_json::Value; use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::runtime::Runtime; use tokio::sync::RwLock; @@ -75,7 +75,7 @@ impl BrowserService { async fn get_or_init_client( client: &Arc>>>, - user_data_dir: &PathBuf, + user_data_dir: &Path, headless: bool, ) -> Result> { if let Some(existing) = client.read().await.as_ref() { @@ -84,7 +84,7 @@ impl BrowserService { let mut client_lock = client.write().await; if client_lock.is_none() { - let new_client = BrowserClient::new(user_data_dir.clone(), headless).await?; + let new_client = BrowserClient::new(user_data_dir.to_path_buf(), headless).await?; *client_lock = Some(Arc::new(new_client)); } diff --git a/tests/integration.rs b/tests/integration.rs index f9d57d7..b9b38a5 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -41,5 +41,5 @@ fn test_version_command() { #[test] fn test_crate_compiles() { // This test passes if the crate compiles successfully - assert!(true); + // The existence of this function proves compilation succeeds }