yncli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ import json
2
+ from typing import List, Dict, Any
3
+
4
+ from yncli.tools.system_tools import run_terminal_command, git_status, git_diff, get_system_info, change_directory, save_plan_document
5
+ from yncli.tools.file_tools import read_file, write_file, edit_file_replace, list_directory, find_files, grep_search
6
+ from yncli.tools.search_tools import web_search, fetch_webpage
7
+ from yncli.tools.polyglot_tools import validate_code_syntax, run_project_tests
8
+
9
+ AGENT_TOOLS: List[Dict[str, Any]] = [
10
+ {
11
+ "type": "function",
12
+ "function": {
13
+ "name": "change_directory",
14
+ "description": "Changes the current active working directory of the workspace (e.g. 'cd laravelcrud' or 'cd ..' or entering a project folder).",
15
+ "parameters": {
16
+ "type": "object",
17
+ "properties": {
18
+ "path": {"type": "string", "description": "Relative or absolute directory path to switch to"}
19
+ },
20
+ "required": ["path"]
21
+ }
22
+ }
23
+ },
24
+ {
25
+ "type": "function",
26
+ "function": {
27
+ "name": "save_plan_document",
28
+ "description": "Saves the complete PRD (Product Requirements Document) with ASCII mockups, database schemas, stack specs, and implementation roadmap into plan.md in the current workspace directory.",
29
+ "parameters": {
30
+ "type": "object",
31
+ "properties": {
32
+ "content": {"type": "string", "description": "The full Markdown content of the PRD to save into plan.md"}
33
+ },
34
+ "required": ["content"]
35
+ }
36
+ }
37
+ },
38
+ {
39
+ "type": "function",
40
+ "function": {
41
+ "name": "web_search",
42
+ "description": "Searches the web in real-time via DuckDuckGo for the latest information, documentation, news, or solutions.",
43
+ "parameters": {
44
+ "type": "object",
45
+ "properties": {
46
+ "query": {"type": "string", "description": "The search query keywords"},
47
+ "max_results": {"type": "integer", "description": "Number of results to return (default: 5)"}
48
+ },
49
+ "required": ["query"]
50
+ }
51
+ }
52
+ },
53
+ {
54
+ "type": "function",
55
+ "function": {
56
+ "name": "fetch_webpage",
57
+ "description": "Fetches a URL and converts its content into clean, readable text/markdown.",
58
+ "parameters": {
59
+ "type": "object",
60
+ "properties": {
61
+ "url": {"type": "string", "description": "The complete HTTP/HTTPS URL"}
62
+ },
63
+ "required": ["url"]
64
+ }
65
+ }
66
+ },
67
+ {
68
+ "type": "function",
69
+ "function": {
70
+ "name": "read_file",
71
+ "description": "Reads file contents with line numbers. Can view entire file or specific line range.",
72
+ "parameters": {
73
+ "type": "object",
74
+ "properties": {
75
+ "file_path": {"type": "string", "description": "Relative or absolute path to the file"},
76
+ "start_line": {"type": "integer", "description": "Optional 1-indexed start line"},
77
+ "end_line": {"type": "integer", "description": "Optional 1-indexed end line"}
78
+ },
79
+ "required": ["file_path"]
80
+ }
81
+ }
82
+ },
83
+ {
84
+ "type": "function",
85
+ "function": {
86
+ "name": "write_file",
87
+ "description": "Creates or completely overwrites a file with new content. Automatically creates parent directories.",
88
+ "parameters": {
89
+ "type": "object",
90
+ "properties": {
91
+ "file_path": {"type": "string", "description": "Relative or absolute path to the file"},
92
+ "content": {"type": "string", "description": "The full code/text content to write"}
93
+ },
94
+ "required": ["file_path", "content"]
95
+ }
96
+ }
97
+ },
98
+ {
99
+ "type": "function",
100
+ "function": {
101
+ "name": "edit_file_replace",
102
+ "description": "Performs surgical search-and-replace edit on an existing file. target_content must uniquely match the section being modified.",
103
+ "parameters": {
104
+ "type": "object",
105
+ "properties": {
106
+ "file_path": {"type": "string", "description": "Relative or absolute path to the file"},
107
+ "target_content": {"type": "string", "description": "Exact text or lines in the file to replace"},
108
+ "replacement_content": {"type": "string", "description": "New replacement text"}
109
+ },
110
+ "required": ["file_path", "target_content", "replacement_content"]
111
+ }
112
+ }
113
+ },
114
+ {
115
+ "type": "function",
116
+ "function": {
117
+ "name": "list_directory",
118
+ "description": "Lists contents of a directory, showing files and subdirectories.",
119
+ "parameters": {
120
+ "type": "object",
121
+ "properties": {
122
+ "dir_path": {"type": "string", "description": "Directory path (default: current directory '.')"},
123
+ "recursive": {"type": "boolean", "description": "If true, lists recursively up to max_depth"},
124
+ "max_depth": {"type": "integer", "description": "Maximum depth for recursive listing (default: 2)"}
125
+ }
126
+ }
127
+ }
128
+ },
129
+ {
130
+ "type": "function",
131
+ "function": {
132
+ "name": "find_files",
133
+ "description": "Finds files matching a glob pattern (e.g. '*.py', '*.ts', 'config*') in the project.",
134
+ "parameters": {
135
+ "type": "object",
136
+ "properties": {
137
+ "pattern": {"type": "string", "description": "Glob pattern (e.g. '*.rs', '*.go')"},
138
+ "search_dir": {"type": "string", "description": "Starting directory (default: '.')"}
139
+ },
140
+ "required": ["pattern"]
141
+ }
142
+ }
143
+ },
144
+ {
145
+ "type": "function",
146
+ "function": {
147
+ "name": "grep_search",
148
+ "description": "Fast text and regex search across all files in the directory tree.",
149
+ "parameters": {
150
+ "type": "object",
151
+ "properties": {
152
+ "query": {"type": "string", "description": "Search keyword or regex pattern"},
153
+ "search_path": {"type": "string", "description": "Path to search within (default: '.')"}
154
+ },
155
+ "required": ["query"]
156
+ }
157
+ }
158
+ },
159
+ {
160
+ "type": "function",
161
+ "function": {
162
+ "name": "run_terminal_command",
163
+ "description": "Runs a terminal command (PowerShell on Windows, Bash on Unix) in the workspace and returns stdout, stderr, and exit code.",
164
+ "parameters": {
165
+ "type": "object",
166
+ "properties": {
167
+ "command": {"type": "string", "description": "Command line string to execute"},
168
+ "timeout": {"type": "integer", "description": "Timeout in seconds (default: 60)"}
169
+ },
170
+ "required": ["command"]
171
+ }
172
+ }
173
+ },
174
+ {
175
+ "type": "function",
176
+ "function": {
177
+ "name": "validate_code_syntax",
178
+ "description": "Validates syntax of a file or code snippet using the appropriate language compiler or linter (Python, TypeScript, Rust, Go, C++, PHP, etc.).",
179
+ "parameters": {
180
+ "type": "object",
181
+ "properties": {
182
+ "language": {"type": "string", "description": "Programming language (e.g. 'python', 'typescript', 'rust', 'go', 'cpp', 'php')"},
183
+ "file_path": {"type": "string", "description": "Optional path to existing file"},
184
+ "code_snippet": {"type": "string", "description": "Optional code string to validate directly"}
185
+ },
186
+ "required": ["language"]
187
+ }
188
+ }
189
+ },
190
+ {
191
+ "type": "function",
192
+ "function": {
193
+ "name": "run_project_tests",
194
+ "description": "Auto-detects and runs project unit tests (pytest, npm test, cargo test, go test, dotnet test).",
195
+ "parameters": {
196
+ "type": "object",
197
+ "properties": {
198
+ "test_filter": {"type": "string", "description": "Optional filter keyword for test cases"}
199
+ }
200
+ }
201
+ }
202
+ },
203
+ {
204
+ "type": "function",
205
+ "function": {
206
+ "name": "git_status",
207
+ "description": "Gets current git repository status (branch, changed files).",
208
+ "parameters": {
209
+ "type": "object",
210
+ "properties": {}
211
+ }
212
+ }
213
+ }
214
+ ]
215
+
216
+
217
+ def execute_tool(name: str, arguments: Dict[str, Any], current_cwd: str = ".") -> Any:
218
+ try:
219
+ if name == "change_directory":
220
+ return change_directory(path=arguments.get("path", "."), current_cwd=current_cwd)
221
+ elif name == "save_plan_document":
222
+ return save_plan_document(content=arguments.get("content", ""), current_cwd=current_cwd)
223
+ elif name == "web_search":
224
+ return web_search(query=arguments.get("query", ""), max_results=arguments.get("max_results", 5))
225
+ elif name == "fetch_webpage":
226
+ return fetch_webpage(url=arguments.get("url", ""))
227
+ elif name == "read_file":
228
+ return read_file(
229
+ file_path=arguments.get("file_path", ""),
230
+ start_line=arguments.get("start_line"),
231
+ end_line=arguments.get("end_line")
232
+ )
233
+ elif name == "write_file":
234
+ return write_file(file_path=arguments.get("file_path", ""), content=arguments.get("content", ""))
235
+ elif name == "edit_file_replace":
236
+ return edit_file_replace(
237
+ file_path=arguments.get("file_path", ""),
238
+ target_content=arguments.get("target_content", ""),
239
+ replacement_content=arguments.get("replacement_content", "")
240
+ )
241
+ elif name == "list_directory":
242
+ return list_directory(
243
+ dir_path=arguments.get("dir_path", "."),
244
+ recursive=arguments.get("recursive", False),
245
+ max_depth=arguments.get("max_depth", 2)
246
+ )
247
+ elif name == "find_files":
248
+ return find_files(pattern=arguments.get("pattern", "*"), search_dir=arguments.get("search_dir", "."))
249
+ elif name == "grep_search":
250
+ return grep_search(query=arguments.get("query", ""), search_path=arguments.get("search_path", "."))
251
+ elif name == "run_terminal_command":
252
+ return run_terminal_command(command=arguments.get("command", ""), timeout=arguments.get("timeout", 60), cwd=current_cwd)
253
+ elif name == "validate_code_syntax":
254
+ return validate_code_syntax(
255
+ language=arguments.get("language", ""),
256
+ file_path=arguments.get("file_path"),
257
+ code_snippet=arguments.get("code_snippet")
258
+ )
259
+ elif name == "run_project_tests":
260
+ return run_project_tests(test_filter=arguments.get("test_filter", ""), cwd=current_cwd)
261
+ elif name == "git_status":
262
+ return git_status(cwd=current_cwd)
263
+ else:
264
+ return f"[ERROR] Unknown tool: {name}"
265
+ except Exception as e:
266
+ return f"[ERROR] Gagal mengeksekusi tool '{name}': {str(e)}"
@@ -0,0 +1,206 @@
1
+ import os
2
+ import re
3
+ import fnmatch
4
+ from pathlib import Path
5
+ from typing import Optional, List, Dict, Any
6
+
7
+ from yncli.clean_text import clean_text_for_terminal
8
+
9
+
10
+ def read_file(file_path: str, start_line: Optional[int] = None, end_line: Optional[int] = None) -> str:
11
+ """
12
+ Reads file content with optional line number range (1-indexed).
13
+ """
14
+ path = Path(file_path).resolve()
15
+ if not path.exists():
16
+ return f"Error: File not found: {file_path}"
17
+ if path.is_dir():
18
+ return f"Error: Path is a directory, not a file: {file_path}"
19
+
20
+ try:
21
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
22
+ lines = f.readlines()
23
+
24
+ total_lines = len(lines)
25
+ if total_lines == 0:
26
+ return f"(File {path.name} is empty)"
27
+
28
+ s_line = max(1, start_line) if start_line is not None else 1
29
+ e_line = min(total_lines, end_line) if end_line is not None else total_lines
30
+
31
+ if s_line > e_line:
32
+ return f"Error: start_line ({s_line}) cannot be greater than end_line ({e_line}). Total lines: {total_lines}"
33
+
34
+ output = []
35
+ output.append(f"File: {path.name} (Lines {s_line}-{e_line} of {total_lines})")
36
+ output.append("-" * 50)
37
+ for i in range(s_line - 1, e_line):
38
+ output.append(f"{i + 1:4d} | {lines[i].rstrip()}")
39
+
40
+ return "\n".join(output)
41
+ except Exception as e:
42
+ return f"Error reading file {file_path}: {str(e)}"
43
+
44
+
45
+ def write_file(file_path: str, content: str) -> str:
46
+ """
47
+ Writes or overwrites content to a file, automatically creating parent directories.
48
+ Sanitizes content to ensure clean UTF-8 without mojibake.
49
+ """
50
+ try:
51
+ path = Path(file_path).resolve()
52
+ path.parent.mkdir(parents=True, exist_ok=True)
53
+ cleaned_content = clean_text_for_terminal(content)
54
+ with open(path, "w", encoding="utf-8") as f:
55
+ f.write(cleaned_content)
56
+ return f"Successfully written {len(cleaned_content.splitlines())} lines to {path}"
57
+ except Exception as e:
58
+ return f"Error writing file {file_path}: {str(e)}"
59
+
60
+
61
+ def edit_file_replace(file_path: str, target_content: str, replacement_content: str) -> str:
62
+ """
63
+ Performs precise surgical search-and-replace edit on an existing file.
64
+ """
65
+ path = Path(file_path).resolve()
66
+ if not path.exists():
67
+ return f"Error: File not found: {file_path}"
68
+
69
+ try:
70
+ with open(path, "r", encoding="utf-8") as f:
71
+ original = f.read()
72
+
73
+ target_norm = clean_text_for_terminal(target_content).replace("\r\n", "\n")
74
+ orig_norm = original.replace("\r\n", "\n")
75
+ repl_norm = clean_text_for_terminal(replacement_content).replace("\r\n", "\n")
76
+
77
+ if target_norm in orig_norm:
78
+ count = orig_norm.count(target_norm)
79
+ if count > 1:
80
+ return f"Error: target_content occurs {count} times in {file_path}. Please provide more unique surrounding lines."
81
+
82
+ new_text = orig_norm.replace(target_norm, repl_norm, 1)
83
+ with open(path, "w", encoding="utf-8") as f:
84
+ f.write(new_text)
85
+ return f"Successfully updated {file_path} with search-and-replace edit."
86
+
87
+ # Fallback: line-by-line whitespace-stripped match
88
+ target_lines = [l.strip() for l in target_norm.strip().split("\n") if l.strip()]
89
+ file_lines = orig_norm.split("\n")
90
+
91
+ match_idx = -1
92
+ for i in range(len(file_lines) - len(target_lines) + 1):
93
+ subset = [file_lines[i + j].strip() for j in range(len(target_lines))]
94
+ if subset == target_lines:
95
+ match_idx = i
96
+ break
97
+
98
+ if match_idx != -1:
99
+ end_match = match_idx + len(target_lines)
100
+ new_file_lines = file_lines[:match_idx] + repl_norm.split("\n") + file_lines[end_match:]
101
+ with open(path, "w", encoding="utf-8") as f:
102
+ f.write("\n".join(new_file_lines))
103
+ return f"Successfully updated {file_path} (matched with whitespace tolerance)."
104
+
105
+ return f"Error: target_content was not found in {file_path}. Please check the exact lines."
106
+ except Exception as e:
107
+ return f"Error editing file {file_path}: {str(e)}"
108
+
109
+
110
+ def list_directory(dir_path: str = ".", recursive: bool = False, max_depth: int = 2) -> str:
111
+ path = Path(dir_path).resolve()
112
+ if not path.exists():
113
+ return f"Error: Directory not found: {dir_path}"
114
+ if not path.is_dir():
115
+ return f"Error: Path is not a directory: {dir_path}"
116
+
117
+ lines = [f"Directory listing for: {path}"]
118
+ lines.append("-" * 50)
119
+
120
+ try:
121
+ if not recursive:
122
+ entries = sorted(path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
123
+ for e in entries:
124
+ if e.name.startswith(".") and e.name not in (".env", ".gitignore", ".skills"):
125
+ continue
126
+ if e.is_dir():
127
+ lines.append(f"[DIR] {e.name}/")
128
+ else:
129
+ size = e.stat().st_size
130
+ lines.append(f"[FILE] {e.name:<30} ({size:,} bytes)")
131
+ else:
132
+ base_depth = len(path.parts)
133
+ for root, dirs, files in os.walk(path):
134
+ cur_path = Path(root)
135
+ depth = len(cur_path.parts) - base_depth
136
+ if depth > max_depth:
137
+ dirs[:] = []
138
+ continue
139
+
140
+ dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("node_modules", "venv", "__pycache__", "target", "vendor", "dist", "build", ".git")]
141
+
142
+ indent = " " * depth
143
+ if depth > 0:
144
+ lines.append(f"{indent}[DIR] {cur_path.name}/")
145
+ for f in sorted(files):
146
+ if not f.startswith("."):
147
+ fpath = cur_path / f
148
+ size = fpath.stat().st_size
149
+ lines.append(f"{indent} [FILE] {f} ({size:,} bytes)")
150
+
151
+ return "\n".join(lines)
152
+ except Exception as e:
153
+ return f"Error listing directory {dir_path}: {str(e)}"
154
+
155
+
156
+ def find_files(pattern: str, search_dir: str = ".") -> str:
157
+ root_path = Path(search_dir).resolve()
158
+ matches = []
159
+ try:
160
+ for root, dirs, files in os.walk(root_path):
161
+ dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("node_modules", "venv", "__pycache__", "target", "vendor", ".git")]
162
+ for f in files:
163
+ if fnmatch.fnmatch(f, pattern):
164
+ rel = Path(root, f).relative_to(root_path)
165
+ matches.append(str(rel))
166
+ if not matches:
167
+ return f"No files matching pattern '{pattern}' found in {search_dir}"
168
+ return f"Found {len(matches)} files matching '{pattern}':\n" + "\n".join(f"- {m}" for m in matches[:50])
169
+ except Exception as e:
170
+ return f"Error finding files: {str(e)}"
171
+
172
+
173
+ def grep_search(query: str, search_path: str = ".", case_sensitive: bool = False) -> str:
174
+ path = Path(search_path).resolve()
175
+ flags = 0 if case_sensitive else re.IGNORECASE
176
+ try:
177
+ pattern = re.compile(query, flags)
178
+ except Exception as e:
179
+ return f"Invalid regex pattern: {str(e)}"
180
+
181
+ matches = []
182
+ try:
183
+ for root, dirs, files in os.walk(path):
184
+ dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("node_modules", "venv", "__pycache__", "target", "vendor", ".git")]
185
+ for f in files:
186
+ fpath = Path(root, f)
187
+ if fpath.suffix.lower() in (".png", ".jpg", ".exe", ".dll", ".zip", ".tar", ".gz", ".pyc", ".pdf"):
188
+ continue
189
+ try:
190
+ with open(fpath, "r", encoding="utf-8", errors="ignore") as file_obj:
191
+ for line_idx, line in enumerate(file_obj, start=1):
192
+ if pattern.search(line):
193
+ rel = fpath.relative_to(path)
194
+ matches.append(f"{rel}:{line_idx}: {line.strip()}")
195
+ if len(matches) >= 50:
196
+ break
197
+ except Exception:
198
+ continue
199
+ if len(matches) >= 50:
200
+ break
201
+
202
+ if not matches:
203
+ return f"No matches found for query '{query}' in {search_path}"
204
+ return f"Found {len(matches)} matches for '{query}':\n" + "\n".join(matches)
205
+ except Exception as e:
206
+ return f"Error running grep search: {str(e)}"
@@ -0,0 +1,133 @@
1
+ import os
2
+ import subprocess
3
+ import tempfile
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from yncli.language_detector import detect_workspace_languages, LANGUAGE_SIGNATURES
8
+ from yncli.tools.system_tools import run_terminal_command
9
+
10
+
11
+ def validate_code_syntax(language: str, file_path: Optional[str] = None, code_snippet: Optional[str] = None) -> str:
12
+ """
13
+ Validates the syntax of a code file or code snippet across various languages (Python, JS/TS, Rust, Go, C/C++, PHP, Ruby, etc.)
14
+ """
15
+ lang_key = language.lower().strip()
16
+
17
+ # Map common aliases
18
+ alias_map = {
19
+ "py": "python",
20
+ "ts": "typescript",
21
+ "js": "javascript",
22
+ "rs": "rust",
23
+ "golang": "golang",
24
+ "go": "golang",
25
+ "c++": "cpp",
26
+ "c": "cpp",
27
+ "cs": "csharp",
28
+ "kt": "java",
29
+ "rb": "ruby",
30
+ }
31
+ lang_key = alias_map.get(lang_key, lang_key)
32
+
33
+ temp_file = None
34
+ target_file = file_path
35
+
36
+ if not target_file and code_snippet:
37
+ ext_map = {
38
+ "python": ".py",
39
+ "typescript": ".ts",
40
+ "javascript": ".js",
41
+ "rust": ".rs",
42
+ "golang": ".go",
43
+ "cpp": ".cpp",
44
+ "csharp": ".cs",
45
+ "java": ".java",
46
+ "php": ".php",
47
+ "ruby": ".rb",
48
+ "dart": ".dart",
49
+ "shell": ".ps1"
50
+ }
51
+ ext = ext_map.get(lang_key, ".txt")
52
+ temp = tempfile.NamedTemporaryFile(delete=False, suffix=ext, mode="w", encoding="utf-8")
53
+ temp.write(code_snippet)
54
+ temp.close()
55
+ temp_file = temp.name
56
+ target_file = temp_file
57
+
58
+ if not target_file or not Path(target_file).exists():
59
+ return "[ERROR] No file or code snippet provided for validation."
60
+
61
+ try:
62
+ # 1. Python Syntax Validation
63
+ if lang_key == "python":
64
+ import py_compile
65
+ try:
66
+ py_compile.compile(target_file, doraise=True)
67
+ return f"[PASS] Python Syntax Check: No syntax errors detected in {Path(target_file).name}."
68
+ except py_compile.PyCompileError as e:
69
+ return f"[FAIL] Python Syntax Error:\n{str(e)}"
70
+
71
+ # 2. General compiler/linter check using system tools
72
+ sig = LANGUAGE_SIGNATURES.get(lang_key)
73
+ if sig and sig.get("syntax_command"):
74
+ cmd = sig["syntax_command"].replace("{file}", f'"{target_file}"')
75
+ result = run_terminal_command(cmd, timeout=15)
76
+
77
+ # Check exit code
78
+ is_exit_zero = "[Process exited with code 0]" in result
79
+ res_lower = result.lower()
80
+
81
+ # Special case for PHP: "no syntax errors detected"
82
+ if "no syntax errors detected" in res_lower and is_exit_zero:
83
+ return f"[PASS] PHP Syntax Check: No syntax errors detected in {Path(target_file).name}."
84
+
85
+ # Determine failure
86
+ has_explicit_error = any(kw in res_lower for kw in [
87
+ "parse error", "syntax error", "fatal error", "compile error",
88
+ "error[e", "compilation failed", "unhandled exception"
89
+ ])
90
+
91
+ if (not is_exit_zero) or has_explicit_error:
92
+ return f"[FAIL] {sig['name']} Syntax / Compiler Check Result:\n{result}"
93
+ else:
94
+ return f"[PASS] {sig['name']} Syntax / Compiler Check Passed:\n{result}"
95
+
96
+ return f"[INFO] Syntax validator not configured or language toolchain not found for '{language}'. File saved."
97
+
98
+ except Exception as e:
99
+ return f"[ERROR] During syntax validation: {str(e)}"
100
+ finally:
101
+ if temp_file and os.path.exists(temp_file):
102
+ try:
103
+ os.remove(temp_file)
104
+ except Exception:
105
+ pass
106
+
107
+
108
+ def run_project_tests(test_filter: str = "", cwd: str = ".") -> str:
109
+ """
110
+ Automatically detects the project type and runs standard test suites (pytest, npm test, cargo test, go test, dotnet test).
111
+ """
112
+ workspace = Path(cwd).resolve()
113
+
114
+ if (workspace / "pytest.ini").exists() or (workspace / "tests").exists() or (workspace / "pyproject.toml").exists():
115
+ cmd = f"pytest {test_filter}" if test_filter else "pytest"
116
+ return run_terminal_command(cmd, timeout=60, cwd=cwd)
117
+ elif (workspace / "package.json").exists():
118
+ cmd = f"npm test -- {test_filter}" if test_filter else "npm test"
119
+ return run_terminal_command(cmd, timeout=60, cwd=cwd)
120
+ elif (workspace / "Cargo.toml").exists():
121
+ cmd = f"cargo test {test_filter}" if test_filter else "cargo test"
122
+ return run_terminal_command(cmd, timeout=60, cwd=cwd)
123
+ elif (workspace / "go.mod").exists():
124
+ cmd = f"go test ./... -run {test_filter}" if test_filter else "go test ./..."
125
+ return run_terminal_command(cmd, timeout=60, cwd=cwd)
126
+ elif list(workspace.glob("*.csproj")) or list(workspace.glob("*.sln")):
127
+ cmd = f"dotnet test --filter {test_filter}" if test_filter else "dotnet test"
128
+ return run_terminal_command(cmd, timeout=60, cwd=cwd)
129
+ elif (workspace / "composer.json").exists() and (workspace / "vendor" / "bin" / "phpunit").exists():
130
+ cmd = f"./vendor/bin/phpunit --filter {test_filter}" if test_filter else "./vendor/bin/phpunit"
131
+ return run_terminal_command(cmd, timeout=60, cwd=cwd)
132
+
133
+ return "[INFO] No known test suite configuration detected in workspace. Use run_terminal_command for custom test scripts."
@@ -0,0 +1,68 @@
1
+ import re
2
+ import warnings
3
+ import urllib.request
4
+ from typing import Optional
5
+
6
+ warnings.filterwarnings("ignore")
7
+
8
+
9
+ def web_search(query: str, max_results: int = 5) -> str:
10
+ """
11
+ Performs a real-time web search using DuckDuckGo to find accurate, up-to-date information.
12
+ """
13
+ try:
14
+ try:
15
+ from ddgs import DDGS
16
+ except ImportError:
17
+ from duckduckgo_search import DDGS
18
+
19
+ ddgs = DDGS()
20
+ results = list(ddgs.text(query, max_results=max_results))
21
+
22
+ if not results:
23
+ return f"No search results found for: '{query}'"
24
+
25
+ formatted = [f"Web Search Results for '{query}':"]
26
+ formatted.append("=" * 50)
27
+ for i, res in enumerate(results, start=1):
28
+ title = res.get("title", "No title")
29
+ link = res.get("href", res.get("link", ""))
30
+ body = res.get("body", res.get("snippet", ""))
31
+ formatted.append(f"[{i}] {title}\nURL: {link}\nSnippet: {body}\n")
32
+
33
+ return "\n".join(formatted)
34
+ except Exception as e:
35
+ return f"Error executing web search: {str(e)}"
36
+
37
+
38
+ def fetch_webpage(url: str, max_chars: int = 6000) -> str:
39
+ """
40
+ Fetches the content of a webpage and extracts clean, readable text/markdown.
41
+ """
42
+ try:
43
+ import requests
44
+ from bs4 import BeautifulSoup
45
+
46
+ headers = {
47
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
48
+ }
49
+ resp = requests.get(url, headers=headers, timeout=15)
50
+ resp.raise_for_status()
51
+
52
+ soup = BeautifulSoup(resp.text, "html.parser")
53
+
54
+ for tag in soup(["script", "style", "nav", "footer", "header", "aside", "svg", "noscript"]):
55
+ tag.decompose()
56
+
57
+ title = soup.title.string.strip() if soup.title else url
58
+ text = soup.get_text(separator="\n")
59
+
60
+ clean_lines = [line.strip() for line in text.splitlines() if line.strip()]
61
+ clean_text = "\n".join(clean_lines)
62
+
63
+ if len(clean_text) > max_chars:
64
+ clean_text = clean_text[:max_chars] + f"\n... [Content truncated, total {len(text)} chars]"
65
+
66
+ return f"Title: {title}\nURL: {url}\n\nContent:\n{clean_text}"
67
+ except Exception as e:
68
+ return f"Error fetching webpage from {url}: {str(e)}"