XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 1
返回提交历史

XFEstudio/gpt4free

Refactor DeepInfra and Qwen providers to enhance token retrieval and reasoning effort handling; remove unused tools from MCP

fb30f83a
hlohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

9 个文件 +113 -425
Modified g4f/Provider/DeepInfra.py +70 -4
@@ -1,17 +1,18 @@
1 1 from __future__ import annotations
2 2
3 import time
3 4 import asyncio
4 5 import requests
5 6
6 7 from ..requests import get_nodriver_session
8 from ..errors import MissingRequirementsError
7 9 from .template import OpenaiTemplate
8 10
9 11 async def get_turnstile_token_async() -> str:
10 12 try:
11 13 import zendriver as zd
12 14 except ImportError:
13 from ..errors import MissingRequirementsError
14 raise MissingRequirementsError('Install "zendriver" package to use DeepInfra without an API key | pip install zendriver')
15 return None
15 16
16 17 async with get_nodriver_session() as session:
17 18 # Generate a token on any model's page; it is valid for the entire domain
@@ -49,6 +50,69 @@ async def get_turnstile_token_async() -> str:
49 50
50 51 return token
51 52
53 def get_turnstile_token() -> str:
54 """
55 Opens the DeepInfra page using DrissionPage to obtain a Turnstile token.
56 Raises MissingRequirementsError if DrissionPage is not installed.
57 """
58 try:
59 from DrissionPage import ChromiumPage, ChromiumOptions
60 except ImportError:
61 raise MissingRequirementsError('Install "DrissionPage" package to use DeepInfra without an API key | pip install DrissionPage')
62
63 co = ChromiumOptions()
64 # Hide the window off-screen
65 co.set_argument('--window-position=-2000,-2000')
66 co.set_argument('--window-size=800,600')
67 co.set_argument('--log-level=3')
68
69 page = ChromiumPage(co)
70
71 try:
72 # Generate a token on any model's page; it is valid for the entire domain
73 page.get('https://deepinfra.com/' + DeepInfra.default_model)
74
75 # Inject JS to block the original request
76 js_block_fetch = """
77 const origFetch = window.fetch;
78 window.fetch = async function(...args) {
79 let url = args[0];
80 if (typeof url === 'string' && url.includes('/chat/completions')) {
81 return new Response('{}', {status: 200});
82 }
83 return origFetch.apply(this, args);
84 };
85 """
86 page.run_js(js_block_fetch)
87
88 # Initiate Turnstile
89 textarea = page.ele('tag:textarea', timeout=15)
90 if not textarea:
91 return ""
92
93 textarea.input('Test')
94 textarea.input('\n')
95
96 # Wait for the challenge to be solved
97 token_input = page.ele('@name=cf-turnstile-response', timeout=20)
98
99 if not token_input:
100 return ""
101
102 token = ""
103 for _ in range(40):
104 token = token_input.attr('value')
105 if token:
106 break
107 time.sleep(0.5)
108
109 return token
110 finally:
111 try:
112 page.quit()
113 except:
114 pass
115
52 116 class DeepInfra(OpenaiTemplate):
53 117 url = "https://deepinfra.com"
54 118 login_url = "https://deepinfra.com/dash/api_keys"
@@ -70,7 +134,6 @@ class DeepInfra(OpenaiTemplate):
70 134 cls.image_models = [model["model_name"] for model in models if model.get("reported_type") == "text-to-image"]
71 135 if cls.live == 0 and cls.models:
72 136 cls.live += 1
73
74 137 return cls.models
75 138
76 139 @classmethod
@@ -82,7 +145,6 @@ class DeepInfra(OpenaiTemplate):
82 145 if headers is None:
83 146 headers = {}
84 147 headers["X-DeepInfra-Turnstile"] = token
85
86 148 async for chunk in super().create_async_generator(model, messages, api_key=api_key, headers=headers, **kwargs):
87 149 yield chunk
88 150
@@ -93,4 +155,8 @@ class DeepInfra(OpenaiTemplate):
93 155 headers["X-Deepinfra-Source"] = "model-embed"
94 156 headers["Origin"] = "https://deepinfra.com"
95 157 headers["Referer"] = "https://deepinfra.com/"
158 if not headers.get("X-DeepInfra-Turnstile"):
159 token = get_turnstile_token()
160 if token:
161 headers["X-DeepInfra-Turnstile"] = token
96 162 return headers
Modified g4f/Provider/Qwen.py +14 -24
@@ -26,13 +26,11 @@ from ..typing import AsyncResult, Messages, MediaListType
26 26
27 27 try:
28 28 import curl_cffi
29
30 29 has_curl_cffi = True
31 30 except ImportError:
32 31 has_curl_cffi = False
33 32 try:
34 33 import zendriver as nodriver
35
36 34 has_nodriver = True
37 35 except ImportError:
38 36 has_nodriver = False
@@ -124,8 +122,6 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
124 122 url = "https://chat.qwen.ai"
125 123 working = True
126 124 active_by_default = True
127 supports_stream = True
128 supports_message_history = False
129 125 image_cache = True
130 126 _models_loaded = True
131 127 image_models = image_models
@@ -372,19 +368,19 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
372 368
373 369 @classmethod
374 370 async def create_async_generator(
375 cls,
376 model: str,
377 messages: Messages,
378 media: MediaListType = None,
379 conversation: JsonConversation = None,
380 proxy: str = None,
381 stream: bool = True,
382 reasoning_effort: Optional[Literal["low", "medium", "high"]] = "medium",
383 chat_type: Literal[
384 "t2t", "search", "artifacts", "web_dev", "deep_research", "t2i", "image_edit", "t2v"
385 ] = "t2t",
386 aspect_ratio: Optional[Literal["1:1", "4:3", "3:4", "16:9", "9:16"]] = None,
387 **kwargs
371 cls,
372 model: str,
373 messages: Messages,
374 media: MediaListType = None,
375 conversation: JsonConversation = None,
376 proxy: str = None,
377 stream: bool = True,
378 reasoning_effort: Optional[Literal["none", "low", "medium", "high", "x-high"]] = "medium",
379 chat_type: Literal[
380 "t2t", "search", "artifacts", "web_dev", "deep_research", "t2i", "image_edit", "t2v"
381 ] = "t2t",
382 aspect_ratio: Optional[Literal["1:1", "4:3", "3:4", "16:9", "9:16"]] = None,
383 **kwargs
388 384 ) -> AsyncResult:
389 385 """
390 386 chat_type:
@@ -539,7 +535,7 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
539 535 except (json.JSONDecodeError, KeyError, IndexError):
540 536 continue
541 537 if usage:
542 yield Usage(**usage)
538 yield Usage.from_dict(usage)
543 539 return
544 540
545 541 except (aiohttp.ClientResponseError, RuntimeError) as e:
@@ -556,10 +552,4 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
556 552 else:
557 553 raise e
558 554 raise RateLimitError("The Qwen provider reached the request limit after 5 attempts.")
559
560 # except CloudflareError as e:
561 # debug.error(f"{cls.__name__}: {e}")
562 # args = await cls.get_args(proxy, **kwargs)
563 # cookie = "; ".join([f"{k}={v}" for k, v in args["cookies"].items()])
564 # continue
565 555 raise RateLimitError("The Qwen provider reached the limit Cloudflare.")
Modified g4f/Provider/needs_auth/DeepSeekAPI.py +6 -3
@@ -2,12 +2,11 @@ from __future__ import annotations
2 2
3 3 import os
4 4 import json
5 import time
6 5 import uuid
7 6 import base64
8 7 from datetime import datetime
9 from typing import AsyncIterator
10 8 from pathlib import Path
9 from typing import Optional, Literal
11 10
12 11 from g4f.typing import AsyncResult, Messages, Cookies
13 12 from g4f.requests import StreamSession, raise_for_status, sse_stream, FormData
@@ -335,6 +334,7 @@ class DeepSeekAPI(AsyncGeneratorProvider, ProviderModelMixin):
335 334 conversation: JsonConversation = None,
336 335 web_search: bool = False,
337 336 media: list = None,
337 reasoning_effort: Optional[Literal["none", "low", "medium", "high", "x-high"]] = None,
338 338 delete_session: bool = False,
339 339 **kwargs
340 340 ) -> AsyncResult:
@@ -412,7 +412,10 @@ class DeepSeekAPI(AsyncGeneratorProvider, ProviderModelMixin):
412 412 prompt = get_last_user_message(messages)
413 413
414 414 # Determine thinking mode
415 thinking_enabled = bool(model) and "deepseek-r1" in model
415 if reasoning_effort is not None and reasoning_effort != "none":
416 thinking_enabled = True
417 else:
418 thinking_enabled = bool(model) and "deepseek-r1" in model
416 419
417 420 yield JsonRequest.from_dict({
418 421 "prompt": prompt,
Modified g4f/Provider/template/OpenaiTemplate.py +2 -1
@@ -10,7 +10,6 @@ from ...image import use_aspect_ratio
10 10 from ...image.copy_images import save_response_media
11 11 from ...providers.response import *
12 12 from ...tools.media import render_messages
13 from ...tools.run_tools import AuthManager
14 13 from ...config import AppConfig
15 14 from ...errors import MissingAuthError
16 15 from ... import debug
@@ -37,6 +36,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
37 36 async def get_quota(cls, api_key: Optional[str] = None, **kwargs) -> dict:
38 37 """Get the quota information for the API key."""
39 38 if not api_key:
39 from ...tools.run_tools import AuthManager
40 40 api_key = AuthManager.load_api_key(cls)
41 41 if api_key and cls.models_needs_auth and cls.quota_url is None:
42 42 cls.quota_url = f"{cls.base_url}/models"
@@ -84,6 +84,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
84 84 if api_key is None and cls.api_key is not None:
85 85 api_key = cls.api_key
86 86 if not api_key or AppConfig.disable_custom_api_key or not cls.is_provider_api_key(api_key):
87 from ...tools.run_tools import AuthManager
87 88 api_key = AuthManager.load_api_key(cls) or api_key
88 89 if base_url is None:
89 90 base_url = cls.base_url
Modified g4f/api/stubs.py +1 -1
@@ -43,7 +43,7 @@ class RequestConfig(BaseModel):
43 43 ]
44 44 ],
45 45 )
46 reasoning_effort: Optional[Literal["low", "medium", "high"]] = None
46 reasoning_effort: Optional[Literal["none", "low", "medium", "high", "x-high"]] = None
47 47 logit_bias: Optional[dict] = None
48 48 audio: Optional[dict] = None
49 49 response_format: Optional[dict] = None
Modified g4f/mcp/__init__.py +0 -8
@@ -15,13 +15,9 @@ from .tools import (
15 15 MarkItDownTool,
16 16 TextToAudioTool,
17 17 WebSearchTool,
18 WebScrapeTool,
19 18 ImageGenerationTool,
20 19 PythonExecuteTool,
21 20 FileReadTool,
22 FileReadLinesTool,
23 FileSearchTool,
24 FileWriteTool,
25 21 FileListTool,
26 22 FileDeleteTool,
27 23 CreateDirectoryTool,
@@ -49,14 +45,10 @@ __all__ = [
49 45 'MarkItDownTool',
50 46 'TextToAudioTool',
51 47 'WebSearchTool',
52 'WebScrapeTool',
53 48 'ImageGenerationTool',
54 49 # New tools
55 50 'PythonExecuteTool',
56 51 'FileReadTool',
57 'FileReadLinesTool',
58 'FileSearchTool',
59 'FileWriteTool',
60 52 'FileListTool',
61 53 'FileDeleteTool',
62 54 'CreateDirectoryTool',
Modified g4f/mcp/pa_provider.py +8 -1
@@ -78,6 +78,9 @@ def get_workspace_dir() -> Path:
78 78 workspace.mkdir(parents=True, exist_ok=True)
79 79 return workspace
80 80
81 def is_hidden_file(path: str) -> bool:
82 """Return True if *path* is a hidden file (starts with a dot)."""
83 return any(part.startswith(".") for part in str(path).replace("\\", "/").split("/"))
81 84
82 85 # ---------------------------------------------------------------------------
83 86 # Whitelisted modules
@@ -599,6 +602,10 @@ class PaProviderRegistry:
599 602 models_list = list(getattr(cls, "models") or [])
600 603 except Exception:
601 604 pass
605 relative_path = pa_path.relative_to(directory).as_posix()
606 print(f"Loaded PA provider: {provider_id} ({relative_path})")
607 if is_hidden_file(relative_path):
608 relative_path = None
602 609 entries.append((
603 610 provider_id,
604 611 getattr(cls, "label", cls.__name__),
@@ -606,7 +613,7 @@ class PaProviderRegistry:
606 613 bool(getattr(cls, "working", True)),
607 614 getattr(cls, "url", None),
608 615 cls,
609 str(pa_path)[len(str(directory))+1:]
616 relative_path
610 617 ))
611 618 except Exception as e:
612 619 debug.error(f"Failed to load PA provider from {pa_path}:", e)
Modified g4f/mcp/server.py +3 -7
@@ -27,9 +27,9 @@ from ..image import EXTENSIONS_MAP
27 27 from ..image.copy_images import get_media_dir, copy_media, get_source_url
28 28
29 29 from .tools import (
30 MarkItDownTool, TextToAudioTool, WebSearchTool, WebScrapeTool, ImageGenerationTool,
31 PythonExecuteTool, FileReadTool, FileReadLinesTool, FileSearchTool,
32 FileWriteTool, FileListTool, FileDeleteTool, ApplyPatchTool,
30 MarkItDownTool, TextToAudioTool, WebSearchTool, ImageGenerationTool,
31 PythonExecuteTool, FileReadTool,
32 FileListTool, FileDeleteTool, ApplyPatchTool,
33 33 CreateDirectoryTool, CreateFileTool, FetchWebpageTool,
34 34 FileSearchGlobTool, GrepSearchTool, GithubRepoTool, GithubTextSearchTool,
35 35 )
@@ -72,16 +72,12 @@ class MCPServer:
72 72 self.safe_mode = safe_mode
73 73 self.tools = {
74 74 'web_search': WebSearchTool(),
75 'web_scrape': WebScrapeTool(),
76 75 'image_generation': ImageGenerationTool(),
77 76 'text_to_audio': TextToAudioTool(),
78 77 'mark_it_down': MarkItDownTool(),
79 78 'python_execute': PythonExecuteTool(safe_mode=safe_mode),
80 79 'apply_patch': ApplyPatchTool(),
81 80 'file_read': FileReadTool(),
82 'file_read_lines': FileReadLinesTool(),
83 'file_search': FileSearchTool(),
84 'file_write': FileWriteTool(),
85 81 'file_list': FileListTool(safe_mode=safe_mode),
86 82 'file_delete': FileDeleteTool(),
87 83 'create_directory': CreateDirectoryTool(),
Modified g4f/mcp/tools.py +9 -376
@@ -6,7 +6,6 @@ This module provides MCP tool implementations that wrap gpt4free capabilities:
6 6 - ImageGenerationTool: Image generation using various AI providers
7 7 - PythonExecuteTool: Safe Python code execution with whitelisted modules
8 8 - FileReadTool: Read files from the ~/.g4f/workspace directory (supports startLine/endLine)
9 - FileReadLinesTool: Read a range of lines from a workspace file
10 9 - FileSearchTool: Search files and file contents in the workspace
11 10 - FileWriteTool: Write files to the ~/.g4f/workspace directory
12 11 - FileListTool: List files in the ~/.g4f/workspace directory
@@ -130,75 +129,6 @@ class WebSearchTool(MCPTool):
130 129 }
131 130
132 131
133 class WebScrapeTool(MCPTool):
134 """Web scraping tool using gpt4free's scraping capabilities"""
135
136 @property
137 def description(self) -> str:
138 return "Scrape and extract text content from a web page URL. Returns cleaned text content with optional word limit."
139
140 @property
141 def input_schema(self) -> Dict[str, Any]:
142 return {
143 "type": "object",
144 "properties": {
145 "url": {
146 "type": "string",
147 "description": "The URL of the web page to scrape"
148 },
149 "max_words": {
150 "type": "integer",
151 "description": "Maximum number of words to extract (default: 1000)",
152 "default": 1000
153 }
154 },
155 "required": ["url"]
156 }
157
158 async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
159 """Execute web scraping
160
161 Returns:
162 Dict[str, Any]: Scraped content or error message
163 """
164 from ..tools.fetch_and_scrape import fetch_and_scrape
165 from aiohttp import ClientSession
166
167 url = arguments.get("url", "")
168 max_words = arguments.get("max_words", 1000)
169
170 if not url:
171 return {
172 "error": "URL parameter is required"
173 }
174
175 try:
176 # Scrape the URL
177 async with ClientSession() as session:
178 content = await fetch_and_scrape(
179 session=session,
180 url=url,
181 max_words=max_words,
182 add_metadata=True
183 )
184
185 if not content:
186 return {
187 "error": "Failed to scrape content from URL"
188 }
189
190 return {
191 "url": url,
192 "content": content,
193 "word_count": len(content.split())
194 }
195
196 except Exception as e:
197 return {
198 "error": f"Scraping failed: {str(e)}"
199 }
200
201
202 132 class ImageGenerationTool(MCPTool):
203 133 """Image generation tool using gpt4free's image generation capabilities"""
204 134
@@ -674,309 +604,6 @@ class FileReadTool(MCPTool):
674 604 return {"error": f"Read failed: {exc}"}
675 605
676 606
677 class FileReadLinesTool(MCPTool):
678 """Read a range of lines from a file inside the workspace."""
679
680 @property
681 def description(self) -> str:
682 return (
683 "Read a range of lines from a text file inside the ~/.g4f/workspace directory. "
684 "Provide a relative path and optional start/end line indexes."
685 )
686
687 @property
688 def input_schema(self) -> Dict[str, Any]:
689 return {
690 "type": "object",
691 "properties": {
692 "path": {
693 "type": "string",
694 "description": "Relative path to the file inside the workspace",
695 },
696 "start_line": {
697 "type": "integer",
698 "description": "1-based first line to read (default: 1)",
699 "default": 1,
700 },
701 "end_line": {
702 "type": "integer",
703 "description": "1-based last line to read (inclusive)",
704 },
705 "max_lines": {
706 "type": "integer",
707 "description": "Maximum number of lines to return when end_line is not provided",
708 "default": 1000,
709 },
710 },
711 "required": ["path"],
712 }
713
714 async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
715 from .pa_provider import get_workspace_dir
716
717 rel_path = arguments.get("path", "")
718 if not rel_path:
719 return {"error": "path parameter is required"}
720
721 start_line = int(arguments.get("start_line", 1))
722 end_line = arguments.get("end_line")
723 max_lines = int(arguments.get("max_lines", 1000))
724
725 if start_line < 1:
726 return {"error": "start_line must be >= 1"}
727
728 if end_line is not None:
729 try:
730 end_line = int(end_line)
731 except (TypeError, ValueError):
732 return {"error": "end_line must be an integer"}
733 if end_line < start_line:
734 return {"error": "end_line must be greater than or equal to start_line"}
735
736 workspace = get_workspace_dir().resolve()
737 try:
738 target = (workspace / rel_path).resolve()
739 if not str(target).startswith(str(workspace)):
740 return {"error": "Access outside the workspace is not allowed"}
741 if not target.exists():
742 return {"error": f"File not found: {rel_path}"}
743 if not target.is_file():
744 return {"error": f"Path is not a file: {rel_path}"}
745
746 lines = target.read_text(encoding="utf-8").splitlines(True)
747 total_lines = len(lines)
748 if start_line > total_lines:
749 return {
750 "path": rel_path,
751 "start_line": start_line,
752 "end_line": end_line,
753 "lines": [],
754 "total_lines": total_lines,
755 }
756
757 start_index = start_line - 1
758 if end_line is None:
759 end_index = min(total_lines, start_index + max_lines)
760 else:
761 end_index = min(total_lines, end_line)
762
763 selected = lines[start_index:end_index]
764 return {
765 "path": rel_path,
766 "start_line": start_line,
767 "end_line": end_index,
768 "lines": selected,
769 "total_lines": total_lines,
770 "returned_lines": len(selected),
771 }
772 except Exception as exc:
773 return {"error": f"Read lines failed: {exc}"}
774
775
776 class FileWriteTool(MCPTool):
777 """Write (or create) a file inside the ``~/.g4f/workspace`` directory."""
778
779 @property
780 def description(self) -> str:
781 return (
782 "Write text content to a file inside the ~/.g4f/workspace directory. "
783 "Creates parent directories as needed. "
784 "Provide a relative path from the workspace root and the content to write."
785 )
786
787 @property
788 def input_schema(self) -> Dict[str, Any]:
789 return {
790 "type": "object",
791 "properties": {
792 "path": {
793 "type": "string",
794 "description": "Relative path to the file inside the workspace",
795 },
796 "content": {
797 "type": "string",
798 "description": "Text content to write to the file",
799 },
800 "append": {
801 "type": "boolean",
802 "description": "If true, append to existing file instead of overwriting (default: false)",
803 "default": False,
804 },
805 },
806 "required": ["path", "content"],
807 }
808
809 async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
810 from .pa_provider import get_workspace_dir
811
812 rel_path = arguments.get("path", "")
813 content = arguments.get("content")
814 append = bool(arguments.get("append", False))
815
816 if not rel_path:
817 return {"error": "path parameter is required"}
818 if content is None:
819 return {"error": "content parameter is required"}
820
821 workspace = get_workspace_dir().resolve()
822 try:
823 target = (workspace / rel_path).resolve()
824 if not str(target).startswith(str(workspace)):
825 return {"error": "Access outside the workspace is not allowed"}
826 target.parent.mkdir(parents=True, exist_ok=True)
827 if append:
828 with open(target, "a", encoding="utf-8") as f:
829 f.write(content)
830 else:
831 target.write_text(content, encoding="utf-8")
832 result: Dict[str, Any] = {
833 "path": rel_path,
834 "size": len(content),
835 "appended": append,
836 }
837 origin = arguments.get("origin")
838 if origin:
839 result["url"] = f"{origin}/pa/files/{rel_path}"
840 return result
841 except Exception as exc:
842 return {"error": f"Write failed: {exc}"}
843
844
845 class FileSearchTool(MCPTool):
846 """Search files and file contents inside the ``~/.g4f/workspace`` directory."""
847
848 @property
849 def description(self) -> str:
850 return (
851 "Search files and contents inside the ~/.g4f/workspace directory. "
852 "Provide a relative path, filename pattern, or text query to search."
853 )
854
855 @property
856 def input_schema(self) -> Dict[str, Any]:
857 return {
858 "type": "object",
859 "properties": {
860 "path": {
861 "type": "string",
862 "description": (
863 "Relative path to a workspace directory to search in "
864 "(default: workspace root)"
865 ),
866 "default": "",
867 },
868 "pattern": {
869 "type": "string",
870 "description": (
871 "File name glob or substring pattern to filter files "
872 "(example: '*.py' or 'README')."
873 ),
874 },
875 "query": {
876 "type": "string",
877 "description": "Text query to search inside file contents.",
878 },
879 "regex": {
880 "type": "boolean",
881 "description": "Interpret the query as a regular expression.",
882 "default": False,
883 },
884 "case_sensitive": {
885 "type": "boolean",
886 "description": "Perform case-sensitive matching for names and content.",
887 "default": False,
888 },
889 "recursive": {
890 "type": "boolean",
891 "description": "Search directories recursively (default: true).",
892 "default": True,
893 },
894 "max_results": {
895 "type": "integer",
896 "description": "Maximum number of matched files to return.",
897 "default": 100,
898 },
899 },
900 "required": [],
901 }
902
903 async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
904 from .pa_provider import get_workspace_dir
905
906 rel_path = arguments.get("path", "") or ""
907 pattern = arguments.get("pattern")
908 query = arguments.get("query")
909 regex = bool(arguments.get("regex", False))
910 case_sensitive = bool(arguments.get("case_sensitive", False))
911 recursive = bool(arguments.get("recursive", True))
912 max_results = int(arguments.get("max_results", 100))
913
914 if not pattern and not query:
915 return {"error": "At least one of pattern or query is required"}
916
917 workspace = get_workspace_dir().resolve()
918 try:
919 target = (workspace / rel_path).resolve() if rel_path else workspace
920 if not str(target).startswith(str(workspace)):
921 return {"error": "Access outside the workspace is not allowed"}
922 if not target.exists():
923 return {"error": f"Directory not found: {rel_path or '/'}"}
924 if not target.is_dir():
925 return {"error": f"Path is not a directory: {rel_path}"}
926
927 if pattern and not case_sensitive:
928 pattern_lower = pattern.lower()
929
930 if query and regex:
931 flags = 0 if case_sensitive else re.IGNORECASE
932 try:
933 query_re = re.compile(query, flags)
934 except re.error as exc:
935 return {"error": f"Invalid regular expression: {exc}"}
936
937 matches = []
938 iterator = target.rglob("*") if recursive else target.iterdir()
939 for entry in sorted(iterator):
940 if not entry.is_file():
941 continue
942
943 name = entry.name
944 if pattern:
945 candidate = name if case_sensitive else name.lower()
946 pattern_to_match = pattern if case_sensitive else pattern_lower
947 if not fnmatch.fnmatch(candidate, pattern_to_match):
948 continue
949
950 if query:
951 try:
952 content = entry.read_text(encoding="utf-8", errors="ignore")
953 except Exception:
954 continue
955 if regex:
956 if not query_re.search(content):
957 continue
958 else:
959 haystack = content if case_sensitive else content.lower()
960 needle = query if case_sensitive else query.lower()
961 if needle not in haystack:
962 continue
963
964 matches.append({
965 "path": str(entry.relative_to(workspace)),
966 "name": name,
967 })
968 if len(matches) >= max_results:
969 break
970
971 return {
972 "path": str(target.relative_to(workspace)),
973 "matches": matches,
974 "count": len(matches),
975 }
976 except Exception as exc:
977 return {"error": f"Search failed: {exc}"}
978
979
980 607 class FileListTool(MCPTool):
981 608 """List files and directories inside the ``~/.g4f/workspace`` directory."""
982 609
@@ -1011,7 +638,7 @@ class FileListTool(MCPTool):
1011 638 }
1012 639
1013 640 async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
1014 from .pa_provider import get_workspace_dir
641 from .pa_provider import get_workspace_dir, is_hidden_file
1015 642
1016 643 rel_path = arguments.get("path", "") or ""
1017 644 recursive = bool(arguments.get("recursive", False))
@@ -1033,6 +660,8 @@ class FileListTool(MCPTool):
1033 660 iterator = target.rglob("*") if recursive else target.iterdir()
1034 661 for entry in sorted(iterator):
1035 662 try:
663 if is_hidden_file(entry):
664 continue
1036 665 rel = str(entry.relative_to(workspace))
1037 666 info: Dict[str, Any] = {
1038 667 "path": rel,
@@ -1294,7 +923,7 @@ class FileSearchGlobTool(MCPTool):
1294 923 }
1295 924
1296 925 async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
1297 from .pa_provider import get_workspace_dir
926 from .pa_provider import get_workspace_dir, is_hidden_file
1298 927
1299 928 pattern = arguments.get("query", "")
1300 929 max_results = int(arguments.get("maxResults", 50))
@@ -1310,6 +939,8 @@ class FileSearchGlobTool(MCPTool):
1310 939 continue
1311 940 rel = entry.relative_to(workspace)
1312 941 rel_str = rel.as_posix()
942 if is_hidden_file(rel_str):
943 continue
1313 944 if fnmatch.fnmatch(rel_str, pattern) or fnmatch.fnmatch(entry.name, pattern):
1314 945 matches.append(rel_str)
1315 946 if len(matches) >= max_results:
@@ -1359,7 +990,7 @@ class GrepSearchTool(MCPTool):
1359 990 }
1360 991
1361 992 async def execute(self, arguments: Dict[str, Any]) -> Dict[str, Any]:
1362 from .pa_provider import get_workspace_dir
993 from .pa_provider import get_workspace_dir, is_hidden_file
1363 994
1364 995 pattern = arguments.get("query", "")
1365 996 is_regexp = bool(arguments.get("isRegexp", False))
@@ -1384,6 +1015,8 @@ class GrepSearchTool(MCPTool):
1384 1015 if not entry.is_file():
1385 1016 continue
1386 1017 rel_str = entry.relative_to(workspace).as_posix()
1018 if is_hidden_file(rel_str):
1019 continue
1387 1020 if include_pattern and not fnmatch.fnmatch(rel_str, include_pattern):
1388 1021 continue
1389 1022 try: