diff --git a/src-tauri/src/human_typing.rs b/src-tauri/src/human_typing.rs index b337770..f76b2de 100644 --- a/src-tauri/src/human_typing.rs +++ b/src-tauri/src/human_typing.rs @@ -260,9 +260,7 @@ impl MarkovTyper { if first_error_pos < self.current.len() { let mut should_correct = false; - if self.last_was_backspace { - should_correct = true; - } else if self.mental_cursor_pos >= self.target.len() { + if self.last_was_backspace || self.mental_cursor_pos >= self.target.len() { should_correct = true; } else if !self.current.is_empty() { let last_char = *self.current.last().unwrap(); @@ -340,10 +338,9 @@ impl MarkovTyper { } let typed_char = if self.rng.random::() < current_prob_error { - let wrong = self + self .keyboard - .get_random_neighbor(char_intended, &mut self.rng); - wrong + .get_random_neighbor(char_intended, &mut self.rng) } else { char_intended }; diff --git a/src-tauri/src/mcp_server.rs b/src-tauri/src/mcp_server.rs index 60dee30..f89159e 100644 --- a/src-tauri/src/mcp_server.rs +++ b/src-tauri/src/mcp_server.rs @@ -337,6 +337,101 @@ impl McpServer { "required": ["profile_id"] }), }, + McpTool { + name: "create_profile".to_string(), + description: "Create a new browser profile".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name for the new profile" + }, + "browser": { + "type": "string", + "enum": ["wayfern", "camoufox"], + "description": "Browser engine to use" + }, + "proxy_id": { + "type": "string", + "description": "Optional proxy UUID to assign" + }, + "group_id": { + "type": "string", + "description": "Optional group UUID to assign" + }, + "tags": { + "type": "array", + "items": { "type": "string" }, + "description": "Optional tags for the profile" + } + }, + "required": ["name", "browser"] + }), + }, + McpTool { + name: "update_profile".to_string(), + description: "Update an existing browser profile's settings".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the profile to update" + }, + "name": { + "type": "string", + "description": "New name for the profile" + }, + "proxy_id": { + "type": "string", + "description": "Proxy UUID to assign (empty string to remove)" + }, + "group_id": { + "type": "string", + "description": "Group UUID to assign (empty string to remove)" + }, + "tags": { + "type": "array", + "items": { "type": "string" }, + "description": "Tags for the profile (replaces existing tags)" + }, + "extension_group_id": { + "type": "string", + "description": "Extension group UUID to assign (empty string to remove)" + }, + "proxy_bypass_rules": { + "type": "array", + "items": { "type": "string" }, + "description": "Proxy bypass rules (replaces existing rules)" + } + }, + "required": ["profile_id"] + }), + }, + McpTool { + name: "delete_profile".to_string(), + description: "Delete a browser profile and all its data".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the profile to delete" + } + }, + "required": ["profile_id"] + }), + }, + McpTool { + name: "list_tags".to_string(), + description: "List all tags used across profiles".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": {}, + "required": [] + }), + }, McpTool { name: "list_proxies".to_string(), description: "List all configured proxies".to_string(), @@ -926,7 +1021,7 @@ impl McpServer { }, McpTool { name: "type_text".to_string(), - description: "Focus an element by CSS selector and type text into it with realistic human-like typing — variable speed, natural errors, and self-corrections.".to_string(), + description: "Focus an element by CSS selector and type text into it. By default uses realistic human-like typing with variable speed, natural errors, and self-corrections. Only set instant=true when you are certain the target does not have bot detection (e.g. browser address bars, developer tools, internal apps) — using instant on public websites risks the profile being flagged as a bot.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { @@ -946,9 +1041,13 @@ impl McpServer { "type": "boolean", "description": "Clear the input before typing (default: true)" }, + "instant": { + "type": "boolean", + "description": "Paste all text at once instead of human typing. WARNING: only use on targets without bot detection — using this on public websites risks the profile being flagged." + }, "wpm": { "type": "number", - "description": "Target words per minute (default: 60)" + "description": "Target words per minute for human typing (default: 80)" } }, "required": ["profile_id", "selector", "text"] @@ -1068,6 +1167,10 @@ impl McpServer { "get_profile" => self.handle_get_profile(&arguments).await, "run_profile" => self.handle_run_profile(&arguments).await, "kill_profile" => self.handle_kill_profile(&arguments).await, + "create_profile" => self.handle_create_profile(&arguments).await, + "update_profile" => self.handle_update_profile(&arguments).await, + "delete_profile" => self.handle_delete_profile(&arguments).await, + "list_tags" => self.handle_list_tags().await, "list_proxies" => self.handle_list_proxies().await, "get_profile_status" => self.handle_get_profile_status(&arguments).await, // Group management @@ -1335,6 +1438,253 @@ impl McpServer { })) } + async fn handle_create_profile( + &self, + arguments: &serde_json::Value, + ) -> Result { + let name = arguments + .get("name") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing name".to_string(), + })?; + let browser = arguments + .get("browser") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing browser".to_string(), + })?; + + if browser != "wayfern" && browser != "camoufox" { + return Err(McpError { + code: -32602, + message: "browser must be 'wayfern' or 'camoufox'".to_string(), + }); + } + + let proxy_id = arguments + .get("proxy_id") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let group_id = arguments + .get("group_id") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let tags: Option> = arguments.get("tags").and_then(|v| { + v.as_array().map(|arr| { + arr + .iter() + .filter_map(|item| item.as_str().map(|s| s.to_string())) + .collect() + }) + }); + + // Pick the latest downloaded version for this browser + let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(); + let versions = registry.get_downloaded_versions(browser); + let version = versions.first().ok_or_else(|| McpError { + code: -32000, + message: format!("No downloaded version found for {browser}. Download it first."), + })?; + + let inner = self.inner.lock().await; + let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { + code: -32000, + message: "MCP server not properly initialized".to_string(), + })?; + + let mut profile = ProfileManager::instance() + .create_profile_with_group( + app_handle, name, browser, version, "stable", proxy_id, None, None, None, group_id, false, + ) + .await + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to create profile: {e}"), + })?; + + if let Some(tags) = tags { + let _ = + ProfileManager::instance().update_profile_tags(app_handle, &profile.name, tags.clone()); + profile.tags = tags; + if let Ok(profiles) = ProfileManager::instance().list_profiles() { + let _ = crate::tag_manager::TAG_MANAGER + .lock() + .map(|manager| manager.rebuild_from_profiles(&profiles)); + } + } + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": format!("Profile '{}' created (id: {})", profile.name, profile.id) + }] + })) + } + + async fn handle_update_profile( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + + let inner = self.inner.lock().await; + let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { + code: -32000, + message: "MCP server not properly initialized".to_string(), + })?; + let pm = ProfileManager::instance(); + + if let Some(new_name) = arguments.get("name").and_then(|v| v.as_str()) { + pm.rename_profile(app_handle, profile_id, new_name) + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to rename profile: {e}"), + })?; + } + + if let Some(proxy_id) = arguments.get("proxy_id").and_then(|v| v.as_str()) { + let pid = if proxy_id.is_empty() { + None + } else { + Some(proxy_id.to_string()) + }; + pm.update_profile_proxy(app_handle.clone(), profile_id, pid) + .await + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to update proxy: {e}"), + })?; + } + + if let Some(group_id) = arguments.get("group_id").and_then(|v| v.as_str()) { + let gid = if group_id.is_empty() { + None + } else { + Some(group_id.to_string()) + }; + pm.assign_profiles_to_group(app_handle, vec![profile_id.to_string()], gid) + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to update group: {e}"), + })?; + } + + if let Some(tags) = arguments.get("tags").and_then(|v| v.as_array()) { + let tag_list: Vec = tags + .iter() + .filter_map(|item| item.as_str().map(|s| s.to_string())) + .collect(); + pm.update_profile_tags(app_handle, profile_id, tag_list) + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to update tags: {e}"), + })?; + if let Ok(profiles) = pm.list_profiles() { + let _ = crate::tag_manager::TAG_MANAGER + .lock() + .map(|manager| manager.rebuild_from_profiles(&profiles)); + } + } + + if let Some(ext_group_id) = arguments.get("extension_group_id").and_then(|v| v.as_str()) { + let eid = if ext_group_id.is_empty() { + None + } else { + Some(ext_group_id.to_string()) + }; + pm.update_profile_extension_group(profile_id, eid) + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to update extension group: {e}"), + })?; + } + + if let Some(rules) = arguments + .get("proxy_bypass_rules") + .and_then(|v| v.as_array()) + { + let rule_list: Vec = rules + .iter() + .filter_map(|item| item.as_str().map(|s| s.to_string())) + .collect(); + pm.update_profile_proxy_bypass_rules(app_handle, profile_id, rule_list) + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to update proxy bypass rules: {e}"), + })?; + } + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": format!("Profile '{profile_id}' updated successfully") + }] + })) + } + + async fn handle_delete_profile( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + + let inner = self.inner.lock().await; + let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { + code: -32000, + message: "MCP server not properly initialized".to_string(), + })?; + + ProfileManager::instance() + .delete_profile(app_handle, profile_id) + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to delete profile: {e}"), + })?; + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": format!("Profile '{profile_id}' deleted successfully") + }] + })) + } + + async fn handle_list_tags(&self) -> Result { + let tags = crate::tag_manager::TAG_MANAGER + .lock() + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to access tag manager: {e}"), + })? + .get_all_tags() + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to get tags: {e}"), + })?; + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": serde_json::to_string_pretty(&tags).unwrap_or_default() + }] + })) + } + async fn handle_list_proxies(&self) -> Result { let proxies = PROXY_MANAGER.get_stored_proxies(); @@ -3356,6 +3706,10 @@ impl McpServer { .get("clear_first") .and_then(|v| v.as_bool()) .unwrap_or(true); + let instant = arguments + .get("instant") + .and_then(|v| v.as_bool()) + .unwrap_or(false); let wpm = arguments.get("wpm").and_then(|v| v.as_f64()); let profile = self.get_running_profile(profile_id)?; @@ -3413,7 +3767,17 @@ impl McpServer { }); } - self.send_human_keystrokes(&ws_url, text, wpm).await?; + if instant { + self + .send_cdp( + &ws_url, + "Input.insertText", + serde_json::json!({ "text": text }), + ) + .await?; + } else { + self.send_human_keystrokes(&ws_url, text, wpm).await?; + } Ok(serde_json::json!({ "content": [{ diff --git a/src/components/integrations-dialog.tsx b/src/components/integrations-dialog.tsx index 6c3c3f6..b1f944e 100644 --- a/src/components/integrations-dialog.tsx +++ b/src/components/integrations-dialog.tsx @@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core"; import { Eye, EyeOff } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { @@ -42,6 +43,7 @@ export function IntegrationsDialog({ isOpen, onClose, }: IntegrationsDialogProps) { + const { t } = useTranslation(); const [settings, setSettings] = useState({ api_enabled: false, api_port: 10108, @@ -329,20 +331,7 @@ export function IntegrationsDialog({ {mcpConfig && ( -
-
- -

- Copy this configuration to your Claude Desktop config file - at{" "} - - ~/.config/claude/claude_desktop_config.json - -

-
- +
                       {showMcpToken
@@ -369,20 +358,9 @@ export function IntegrationsDialog({
                       />
                     
- -
- -
    -
  • list_profiles - List browser profiles
  • -
  • run_profile - Launch a browser
  • -
  • kill_profile - Stop a running browser
  • -
  • get_profile_status - Check if browser is running
  • -
  • list_groups, create_group, etc. - Manage groups
  • -
  • list_proxies, create_proxy, etc. - Manage proxies
  • -
-
+

+ {t("integrations.mcpCopyHint")} +

)} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f42efa2..cb72e39 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -384,7 +384,8 @@ "token": "MCP Token", "config": "MCP Configuration", "copyConfig": "Copy Configuration" - } + }, + "mcpCopyHint": "Add this to your MCP client config to connect." }, "import": { "title": "Import Profile", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index c517f79..e3b2a2c 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -384,7 +384,8 @@ "token": "Token MCP", "config": "Configuración MCP", "copyConfig": "Copiar Configuración" - } + }, + "mcpCopyHint": "Agrega esto a la configuración de tu cliente MCP para conectarte." }, "import": { "title": "Importar Perfil", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index ba5714d..2e39070 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -384,7 +384,8 @@ "token": "Jeton MCP", "config": "Configuration MCP", "copyConfig": "Copier la configuration" - } + }, + "mcpCopyHint": "Ajoutez ceci à la configuration de votre client MCP pour vous connecter." }, "import": { "title": "Importer un profil", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index a089c90..202fe07 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -384,7 +384,8 @@ "token": "MCPトークン", "config": "MCP設定", "copyConfig": "設定をコピー" - } + }, + "mcpCopyHint": "MCPクライアントの設定にこれを追加して接続してください。" }, "import": { "title": "プロファイルをインポート", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 2df31d5..e276364 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -384,7 +384,8 @@ "token": "Token MCP", "config": "Configuração MCP", "copyConfig": "Copiar Configuração" - } + }, + "mcpCopyHint": "Adicione isso à configuração do seu cliente MCP para conectar." }, "import": { "title": "Importar Perfil", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 1427132..f2ef9ad 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -384,7 +384,8 @@ "token": "MCP токен", "config": "Конфигурация MCP", "copyConfig": "Копировать конфигурацию" - } + }, + "mcpCopyHint": "Добавьте это в конфигурацию вашего MCP-клиента для подключения." }, "import": { "title": "Импорт профиля", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 9d474ff..89db2a9 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -384,7 +384,8 @@ "token": "MCP 令牌", "config": "MCP 配置", "copyConfig": "复制配置" - } + }, + "mcpCopyHint": "将此添加到您的MCP客户端配置中以进行连接。" }, "import": { "title": "导入配置文件",