vmcode-cli 1.0.0

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.
Files changed (56) hide show
  1. package/INSTALLATION_METHODS.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +199 -0
  4. package/bin/npm-wrapper.js +171 -0
  5. package/bin/rg +0 -0
  6. package/bin/rg.exe +0 -0
  7. package/config.yaml.example +159 -0
  8. package/package.json +42 -0
  9. package/requirements.txt +7 -0
  10. package/scripts/install.js +132 -0
  11. package/setup.bat +114 -0
  12. package/setup.sh +135 -0
  13. package/src/__init__.py +4 -0
  14. package/src/core/__init__.py +1 -0
  15. package/src/core/agentic.py +2342 -0
  16. package/src/core/chat_manager.py +1201 -0
  17. package/src/core/config_manager.py +269 -0
  18. package/src/core/init.py +161 -0
  19. package/src/core/sub_agent.py +174 -0
  20. package/src/exceptions.py +75 -0
  21. package/src/llm/__init__.py +1 -0
  22. package/src/llm/client.py +149 -0
  23. package/src/llm/config.py +445 -0
  24. package/src/llm/prompts.py +569 -0
  25. package/src/llm/providers.py +402 -0
  26. package/src/llm/token_tracker.py +220 -0
  27. package/src/ui/__init__.py +1 -0
  28. package/src/ui/banner.py +103 -0
  29. package/src/ui/commands.py +489 -0
  30. package/src/ui/displays.py +167 -0
  31. package/src/ui/main.py +351 -0
  32. package/src/ui/prompt_utils.py +162 -0
  33. package/src/utils/__init__.py +1 -0
  34. package/src/utils/editor.py +158 -0
  35. package/src/utils/gitignore_filter.py +149 -0
  36. package/src/utils/logger.py +254 -0
  37. package/src/utils/markdown.py +32 -0
  38. package/src/utils/settings.py +94 -0
  39. package/src/utils/tools/__init__.py +55 -0
  40. package/src/utils/tools/command_executor.py +217 -0
  41. package/src/utils/tools/create_file.py +143 -0
  42. package/src/utils/tools/definitions.py +193 -0
  43. package/src/utils/tools/directory.py +374 -0
  44. package/src/utils/tools/file_editor.py +345 -0
  45. package/src/utils/tools/file_helpers.py +109 -0
  46. package/src/utils/tools/file_reader.py +331 -0
  47. package/src/utils/tools/formatters.py +458 -0
  48. package/src/utils/tools/parallel_executor.py +195 -0
  49. package/src/utils/validation.py +117 -0
  50. package/src/utils/web_search.py +71 -0
  51. package/vmcode-proxy/.env.example +5 -0
  52. package/vmcode-proxy/README.md +235 -0
  53. package/vmcode-proxy/package-lock.json +947 -0
  54. package/vmcode-proxy/package.json +20 -0
  55. package/vmcode-proxy/server.js +248 -0
  56. package/vmcode-proxy/server.js.bak +157 -0
@@ -0,0 +1,55 @@
1
+ """Tool execution utilities.
2
+
3
+ This package provides command execution, file editing, and result formatting
4
+ capabilities for the vmCode AI assistant.
5
+ """
6
+
7
+ # Command execution
8
+ from .command_executor import (
9
+ confirm_tool,
10
+ run_shell_command,
11
+ )
12
+ # File editing
13
+ from .file_editor import (
14
+ _resolve_repo_path,
15
+ preview_edit_file,
16
+ run_edit_file,
17
+ )
18
+
19
+ # Result formatting
20
+ from .formatters import (
21
+ format_tool_result,
22
+ format_file_result,
23
+ _build_diff,
24
+ _detect_newline,
25
+ )
26
+
27
+ # File operations
28
+ from .directory import list_directory
29
+ from .create_file import create_file
30
+ from .file_reader import read_file
31
+
32
+ # Tool definitions
33
+ from .definitions import TOOLS, _tools_for_mode
34
+
35
+ __all__ = [
36
+ # Command execution
37
+ 'confirm_tool',
38
+ 'run_shell_command',
39
+ # File editing
40
+ '_resolve_repo_path',
41
+ 'preview_edit_file',
42
+ 'run_edit_file',
43
+ # Formatters
44
+ 'format_tool_result',
45
+ 'format_file_result',
46
+ '_build_diff',
47
+ '_detect_newline',
48
+ # File operations
49
+ 'read_file',
50
+ 'list_directory',
51
+ 'create_file',
52
+ # Tool definitions
53
+ 'TOOLS',
54
+ '_tools_for_mode',
55
+ ]
@@ -0,0 +1,217 @@
1
+ """Command execution via shell (PowerShell on Windows, /bin/sh on Unix/Linux) or direct execution (rg)."""
2
+
3
+ import subprocess
4
+ import shlex
5
+ import os
6
+ from pathlib import Path
7
+ from rich.panel import Panel
8
+ from llm.config import TOOLS_REQUIRE_CONFIRMATION
9
+ from utils.settings import tool_settings
10
+ from exceptions import CommandExecutionError
11
+
12
+ from .formatters import format_tool_result
13
+
14
+
15
+ def normalize_command(command, rg_exe_path):
16
+ """Parse command and return (executable, args_list, needs_shell).
17
+
18
+ Returns:
19
+ tuple: (executable_path, args_list, needs_shell)
20
+ - executable_path: Path object for rg.exe, or None for shell commands
21
+ - args_list: List of arguments for direct execution, or command string for shell
22
+ - needs_shell: Boolean indicating if command should run through shell
23
+ """
24
+ command = command.strip()
25
+
26
+ # Handle rg commands
27
+ if command.startswith("rg ") or command == "rg":
28
+ if command == "rg":
29
+ return rg_exe_path, [], False
30
+
31
+ args_str = command[3:].strip() # Everything after "rg "
32
+ # Parse only the arguments, not the full command string.
33
+ # On Windows, posix=False preserves backslashes in paths.
34
+ use_posix = os.name != "nt"
35
+ args = shlex.split(args_str, posix=use_posix) if args_str else []
36
+ args = [arg[1:-1] if len(arg) >= 2 and arg[0] == arg[-1] and arg[0] in ("'", '"') else arg for arg in args]
37
+ return rg_exe_path, args, False
38
+
39
+ # Other commands go through shell
40
+ return None, command, True
41
+
42
+
43
+ def confirm_tool(command, console, reason=None, requires_approval=True, prompt_session=None):
44
+ """Prompt user for tool execution confirmation.
45
+
46
+ Args:
47
+ command: Command to execute
48
+ console: Rich console for output
49
+ reason: Optional reason for requiring confirmation
50
+ requires_approval: Whether this command specifically requires approval (overrides global flag when True)
51
+ prompt_session: PromptSession instance for input (optional, for Linux compatibility)
52
+
53
+ Returns:
54
+ tuple: (action, guidance_text) where action is "execute", "reject", or "guide"
55
+ and guidance_text contains the user's input when action is "guide"
56
+ """
57
+ # Skip confirmation only if: global flag is off AND command doesn't require approval
58
+ if not TOOLS_REQUIRE_CONFIRMATION and not requires_approval:
59
+ return ("execute", None)
60
+
61
+ # Simple title line with tool details
62
+ console.print("[cyan]───[/][bold white] Tool Confirmation [/][cyan]───[/]")
63
+ if reason:
64
+ console.print(f"Tool request: {command}")
65
+ console.print(f"Details: {reason}")
66
+ else:
67
+ console.print(f"Tool request: {command}")
68
+ console.print("[bold white]Approve tool? (y/n/guidance):[/]")
69
+
70
+ try:
71
+ # Use prompt_session.prompt() if available (for Linux compatibility)
72
+ if prompt_session:
73
+ response = prompt_session.prompt("> ").strip()
74
+ else:
75
+ response = input("> ").strip()
76
+ except (EOFError, OSError):
77
+ # stdin not available - reject command by default
78
+ console.print("[red]User input not available - command rejected[/red]")
79
+ return ("reject", None)
80
+
81
+ console.print()
82
+
83
+ if response.lower() in ("y", "yes"):
84
+ return ("execute", None)
85
+ elif response.lower() in ("n", "no"):
86
+ return ("reject", None)
87
+ else:
88
+ return ("guide", response)
89
+
90
+
91
+ def _prepare_execution_environment(repo_root, rg_exe_path):
92
+ """Prepare environment variables for command execution.
93
+
94
+ Returns:
95
+ dict: Environment variables with updated PATH
96
+ """
97
+ env = os.environ.copy()
98
+ rg_parent = Path(rg_exe_path).parent if rg_exe_path else None
99
+
100
+ if rg_parent and rg_parent.exists():
101
+ bin_path = str(rg_parent)
102
+ else:
103
+ bin_path = str(repo_root / "bin")
104
+
105
+ env["PATH"] = f"{bin_path}{os.pathsep}{env.get('PATH', '')}"
106
+ return env
107
+
108
+
109
+ def _execute_direct_command(cmd_list, repo_root, env, debug_mode, console):
110
+ """Execute command directly (rg.exe) without PowerShell.
111
+
112
+ Returns:
113
+ subprocess.CompletedProcess
114
+ """
115
+ if debug_mode and console:
116
+ console.print(f"[dim]→ Executing: {cmd_list}[/dim]")
117
+ console.print(f"[dim]→ Working dir: {repo_root}[/dim]")
118
+
119
+ result = subprocess.run(
120
+ cmd_list,
121
+ capture_output=True,
122
+ text=True,
123
+ encoding='utf-8',
124
+ errors='replace',
125
+ timeout=tool_settings.command_timeout_sec,
126
+ cwd=str(repo_root),
127
+ env=env,
128
+ )
129
+
130
+ if debug_mode and console:
131
+ console.print(f"[dim]→ Exit code: {result.returncode}[/dim]")
132
+
133
+ return result
134
+
135
+
136
+ def _execute_shell_command(command, repo_root, env, debug_mode, console):
137
+ """Execute command via shell (PowerShell on Windows, /bin/sh on Unix/Linux).
138
+
139
+ Returns:
140
+ subprocess.CompletedProcess
141
+ """
142
+ # Detect platform and use appropriate shell
143
+ is_windows = os.name == 'nt'
144
+
145
+ if is_windows:
146
+ shell_cmd = ["powershell", "-NoProfile", "-NonInteractive", "-Command", str(command)]
147
+ shell_name = "PowerShell"
148
+ else:
149
+ shell_cmd = ["/bin/sh", "-c", str(command)]
150
+ shell_name = "/bin/sh"
151
+
152
+ if debug_mode and console:
153
+ console.print(f"[dim]→ Executing via {shell_name}: {command}[/dim]")
154
+ console.print(f"[dim]→ Working dir: {repo_root}[/dim]")
155
+
156
+ result = subprocess.run(
157
+ shell_cmd,
158
+ capture_output=True,
159
+ text=True,
160
+ encoding='utf-8',
161
+ errors='replace',
162
+ timeout=tool_settings.command_timeout_sec,
163
+ cwd=str(repo_root),
164
+ env=env,
165
+ )
166
+
167
+ if debug_mode and console:
168
+ console.print(f"[dim]→ Exit code: {result.returncode}[/dim]")
169
+
170
+ return result
171
+
172
+
173
+ def run_shell_command(command, repo_root, rg_exe_path, console, debug_mode, gitignore_spec=None):
174
+ """Execute command via rg (direct) or shell (PowerShell on Windows, /bin/sh on Unix/Linux).
175
+
176
+ Args:
177
+ command: Command string to execute
178
+ repo_root: Path to repository root
179
+ rg_exe_path: Path to rg.exe
180
+ console: Rich console for output
181
+ debug_mode: Whether to show debug output
182
+
183
+ Returns:
184
+ str: Formatted tool result
185
+
186
+ Raises:
187
+ CommandExecutionError: If command execution fails
188
+ """
189
+ try:
190
+ env = _prepare_execution_environment(repo_root, rg_exe_path)
191
+ executable, args, needs_shell = normalize_command(command, rg_exe_path)
192
+
193
+ if not needs_shell:
194
+ # Direct execution (rg)
195
+ cmd_list = [str(executable)] + args
196
+ result = _execute_direct_command(cmd_list, repo_root, env, debug_mode, console)
197
+ # AI gets truncated results (via format_tool_result); user sees summary via _display_tool_feedback
198
+ formatted_result = format_tool_result(result, command=command, is_rg=True, debug_mode=True)
199
+ else:
200
+ # Shell execution (PowerShell on Windows, /bin/sh on Unix/Linux)
201
+ result = _execute_shell_command(args, repo_root, env, debug_mode, console)
202
+ # AI gets full results; user sees summary via _display_tool_feedback
203
+ formatted_result = format_tool_result(result, command=command, debug_mode=True)
204
+
205
+ if debug_mode and console:
206
+ console.print()
207
+ console.print(f"[dim]→ AI receives:\n{formatted_result}[/dim]")
208
+
209
+ return formatted_result
210
+ except CommandExecutionError:
211
+ # Re-raise our custom exceptions
212
+ raise
213
+ except Exception as exc:
214
+ raise CommandExecutionError(
215
+ f"Command execution failed",
216
+ details={"command": command, "original_error": str(exc)}
217
+ )
@@ -0,0 +1,143 @@
1
+ """File creation operations."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional, Tuple
6
+
7
+ from utils.settings import MAX_FILE_PREVIEW_LINES
8
+ from .file_helpers import (
9
+ _is_fast_ignored,
10
+ _is_ignored_cached,
11
+ _register_gitignore_spec,
12
+ _is_reserved_windows_name
13
+ )
14
+ from .formatters import format_file_result
15
+
16
+
17
+ def _validate_create_path(
18
+ path_str: str,
19
+ repo_root: Path,
20
+ gitignore_spec
21
+ ) -> Tuple[Optional[Path], Optional[str]]:
22
+ """Validate and resolve path for file creation.
23
+
24
+ Args:
25
+ path_str: Path string to validate
26
+ repo_root: Repository root directory
27
+ gitignore_spec: Optional PathSpec for .gitignore filtering
28
+
29
+ Returns:
30
+ (resolved_path, error_message) - error_message is None if valid
31
+
32
+ Checks:
33
+ - Windows filename validation (invalid chars, reserved names)
34
+ - Path resolution
35
+ - Gitignore filtering (only within repo)
36
+ """
37
+ try:
38
+ # Windows validation
39
+ if os.name == 'nt':
40
+ invalid_chars = '<>:"|?*'
41
+ if any(char in path_str for char in invalid_chars):
42
+ return None, f"Filename contains invalid characters: {invalid_chars}"
43
+
44
+ filename = Path(path_str).name
45
+ if _is_reserved_windows_name(filename):
46
+ return None, f"Filename is a reserved Windows device name: {filename}"
47
+
48
+ # Resolve path
49
+ raw_path = Path(path_str)
50
+ if not raw_path.is_absolute():
51
+ raw_path = repo_root / raw_path
52
+ resolved = raw_path.resolve()
53
+
54
+ # Check gitignore (only applies to paths within repo)
55
+ if gitignore_spec is not None:
56
+ if _is_fast_ignored(resolved):
57
+ return None, f"File blocked by .gitignore: {resolved.relative_to(repo_root)}"
58
+
59
+ spec_key = _register_gitignore_spec(gitignore_spec)
60
+ if _is_ignored_cached(str(resolved), str(repo_root), spec_key):
61
+ return None, f"File blocked by .gitignore: {resolved.relative_to(repo_root)}"
62
+
63
+ return resolved, None
64
+
65
+ except Exception as e:
66
+ return None, str(e)
67
+
68
+
69
+ def create_file(
70
+ path_str: str,
71
+ repo_root: Path,
72
+ content: Optional[str] = None,
73
+ gitignore_spec = None
74
+ ) -> str:
75
+ """Create a new file with optional initial content.
76
+
77
+ Creates a new file at the specified path, creating parent directories
78
+ if needed. The file must not already exist. Respects .gitignore.
79
+
80
+ Args:
81
+ path_str: Path string to the file to create
82
+ repo_root: Repository root directory (for path resolution)
83
+ content: Optional initial content for the file. If omitted, creates empty file.
84
+ gitignore_spec: Optional PathSpec for .gitignore filtering
85
+
86
+ Returns:
87
+ str: Formatted result with exit_code and status, including preview
88
+ """
89
+ try:
90
+ # Validate path
91
+ resolved, error = _validate_create_path(path_str, repo_root, gitignore_spec)
92
+ if error:
93
+ return format_file_result(exit_code=1, error=error, path=path_str)
94
+
95
+ # Check if already exists
96
+ if resolved.exists():
97
+ return format_file_result(
98
+ exit_code=1,
99
+ error="File already exists",
100
+ path=str(resolved.relative_to(repo_root))
101
+ )
102
+
103
+ # Create parent directories if needed
104
+ parent_dir = resolved.parent
105
+ if parent_dir != repo_root and not parent_dir.exists():
106
+ parent_dir.mkdir(parents=True, exist_ok=True)
107
+
108
+ # Write content or create empty file
109
+ if content is not None:
110
+ resolved.write_text(content, encoding="utf-8", newline="")
111
+ else:
112
+ content = ""
113
+ resolved.touch()
114
+
115
+ # Build result with content for display (truncate preview if needed)
116
+ result_lines = []
117
+ result_lines.append(f"exit_code=0")
118
+ result_lines.append(f"path={resolved.relative_to(repo_root)}")
119
+ result_lines.append(f"content=File created successfully")
120
+ result_lines.append("")
121
+ result_lines.append(f"=== FILE_CONTENT ===")
122
+
123
+ # Truncate content for preview if it exceeds max lines
124
+ if content:
125
+ content_lines = content.splitlines(keepends=True)
126
+ if len(content_lines) > MAX_FILE_PREVIEW_LINES:
127
+ truncated_content = "".join(content_lines[:MAX_FILE_PREVIEW_LINES])
128
+ omitted = len(content_lines) - MAX_FILE_PREVIEW_LINES
129
+ result_lines.append(truncated_content)
130
+ result_lines.append(f"\n... ({omitted} more lines omitted from preview)")
131
+ else:
132
+ result_lines.append(content)
133
+
134
+ result_lines.append("=== END_FILE_CONTENT ===")
135
+
136
+ return "\n".join(result_lines) + "\n\n"
137
+
138
+ except PermissionError:
139
+ return format_file_result(exit_code=1, error="Permission denied", path=path_str)
140
+ except OSError as e:
141
+ return format_file_result(exit_code=1, error=f"Invalid filename: {e}", path=path_str)
142
+ except Exception as e:
143
+ return format_file_result(exit_code=1, error=str(e), path=path_str)
@@ -0,0 +1,193 @@
1
+ """Tool definitions for OpenAI-style function calling.
2
+
3
+ This module contains the schema definitions for all available tools,
4
+ and utilities to filter them based on interaction mode.
5
+ """
6
+
7
+ # Tool definition for OpenAI-style function calling
8
+ TOOLS = [
9
+ {
10
+ "type": "function",
11
+ "function": {
12
+ "name": "rg",
13
+ "description": "A powerful search tool built on ripgrep. Works on any directory in the filesystem.\n\n**Usage:**\n- ALWAYS use rg for search tasks. NEVER invoke `grep` or `rg` as a shell command. The rg tool has been optimized for correct permissions and access.\n- Supports full regex syntax (e.g., \"log.*Error\", \"function\\s+\\w+\")\n- Filter files with glob parameter (e.g., \"*.js\", \"**/*.tsx\") or type parameter (e.g., \"js\", \"py\", \"rust\")\n- Output modes: \"content\" shows matching lines, \"files_with_matches\" shows only file paths (default), \"count\" shows match counts\n- Use sub_agent tool for open-ended searches requiring multiple rounds\n- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface\\{\\}` to find `interface{}` in Go code)\n- Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct \\{[\\s\\S]*?field`, use `multiline: true`",
14
+ "parameters": {
15
+ "type": "object",
16
+ "properties": {
17
+ "pattern": {"type": "string", "description": "The regular expression pattern to search for in file contents"},
18
+ "path": {"type": "string", "description": "File or directory to search in (rg PATH). Defaults to current working directory. Works anywhere on the filesystem."},
19
+ "glob": {"type": "string", "description": "Glob pattern to filter files (e.g. \"*.js\", \"*.{ts,tsx}\") - maps to rg --glob"},
20
+ "output_mode": {"type": "string", "enum": ["content", "files_with_matches", "count"], "description": "Output mode: \"content\" shows matching lines (supports -B/-A/-C context, -n line numbers), \"files_with_matches\" shows file paths, \"count\" shows match counts. Defaults to \"files_with_matches\"."},
21
+ "-B": {"type": "number", "description": "Number of lines to show before each match (rg -B). Requires output_mode: \"content\", ignored otherwise."},
22
+ "-A": {"type": "number", "description": "Number of lines to show after each match (rg -A). Requires output_mode: \"content\", ignored otherwise."},
23
+ "-C": {"type": "number", "description": "Number of lines to show before and after each match (rg -C). Requires output_mode: \"content\", ignored otherwise."},
24
+ "-n": {"type": "boolean", "description": "Show line numbers in output (rg -n). Requires output_mode: \"content\", ignored otherwise."},
25
+ "-i": {"type": "boolean", "description": "Case insensitive search (rg -i)"},
26
+ "type": {"type": "string", "description": "File type to search (rg --type). Common types: js, py, rust, go, java, etc. More efficient than include for standard file types."},
27
+ "multiline": {"type": "boolean", "description": "Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false."}
28
+ },
29
+ "required": ["pattern"]
30
+ }
31
+ }
32
+ },
33
+ {
34
+ "type": "function",
35
+ "function": {
36
+ "name": "execute_command",
37
+ "description": "Execute shell commands for git, system tasks, debugging, and file operations.\n\n**Use for:**\n- Git operations: git clone, pull, push, status, etc.\n- System debugging: ps, lsof, netstat, journalctl, systemctl\n- File operations: rm, mv, cp, mkdir (system-wide)\n- Network tools: ping, curl, wget, ssh\n- Package management: pacman, pip, npm, apt\n- Path navigation: cd /path && command (use && for chaining)\n\n**Important:**\n- All commands execute from repository root\n- Use && for conditional chaining (stops on error)\n- Absolute paths allowed for system debugging\n\n**Do NOT use for:**\n- Code search (use rg tool)\n- Reading files (use read_file)\n- Listing directories (use list_directory)\n- Creating/editing files (use create_file/edit_file)\n- NO chaining with ;, |, >, <, ` (only && allowed)",
38
+ "parameters": {
39
+ "type": "object",
40
+ "properties": {
41
+ "command": {"type": "string", "description": "Command to execute. Examples: 'git status', 'ps aux', 'cd /var/log && tail -f syslog'"}
42
+ },
43
+ "required": ["command"]
44
+ }
45
+ }
46
+ },
47
+ {
48
+ "type": "function",
49
+ "function": {
50
+ "name": "read_file",
51
+ "description": "Read file contents using Python file reader. Use this to view a file (or a specific line range). Prefer this over rg when you already know the file path. Works on any file in the filesystem.",
52
+ "parameters": {
53
+ "type": "object",
54
+ "properties": {
55
+ "path": {"type": "string", "description": "Path to read (works anywhere on filesystem)"},
56
+ "max_lines": {"type": "integer", "description": "Max lines to read (omit for full file)"},
57
+ "start_line": {"type": "integer", "description": "1-based starting line number (default: 1). Use with max_lines to read a specific excerpt."}
58
+ },
59
+ "required": ["path"]
60
+ }
61
+ }
62
+ },
63
+ {
64
+ "type": "function",
65
+ "function": {
66
+ "name": "list_directory",
67
+ "description": "List directory contents using Python file lister (preferred over PowerShell). Works on any directory in the filesystem.",
68
+ "parameters": {
69
+ "type": "object",
70
+ "properties": {
71
+ "path": {"type": "string", "description": "Path to list (default: '.', works anywhere on filesystem)"},
72
+ "recursive": {"type": "boolean", "description": "List recursively (default: false)"},
73
+ "show_files": {"type": "boolean", "description": "Include files (default: true)"},
74
+ "show_dirs": {"type": "boolean", "description": "Include directories (default: true)"},
75
+ "pattern": {"type": "string", "description": "Glob pattern to filter results (e.g., \"*.py\")"}
76
+ },
77
+ "required": ["path"]
78
+ }
79
+ }
80
+ },
81
+ {
82
+ "type": "function",
83
+ "function": {
84
+ "name": "create_file",
85
+ "description": "Create a new file with optional initial content. File must not exist. For small files, include content directly. Creates a preview of the written content (up to 200 lines) with syntax highlighting. Works on any path in the filesystem.",
86
+ "parameters": {
87
+ "type": "object",
88
+ "properties": {
89
+ "path": {"type": "string", "description": "Path to create (works anywhere on filesystem)"}, "content": {"type": "string", "description": "Initial content (omit for empty file)"}
90
+ },
91
+ "required": ["path"]
92
+ }
93
+ }
94
+ }, {
95
+ "type": "function",
96
+ "function": {
97
+ "name": "edit_file",
98
+ "description": "Apply search/replace edit to file. Search text must appear exactly once. Works on any file in the filesystem.",
99
+ "parameters": {
100
+ "type": "object",
101
+ "properties": {
102
+ "path": {"type": "string", "description": "Path to edit (works anywhere on filesystem)"},
103
+ "search": {"type": "string", "description": "Exact text to find. Must be unique. Include context. Multi-line supported."},
104
+ "replace": {"type": "string", "description": "Replacement text. Multi-line supported."},
105
+ "context_lines": {"type": "integer", "description": "Context lines in diff (default: 3)"},
106
+ "color": {"type": "string", "description": "Color mode: 'auto', 'on', 'off' (default: 'auto')"}
107
+ },
108
+ "required": ["path", "search", "replace"]
109
+ }
110
+ }
111
+ },
112
+ {
113
+ "type": "function",
114
+ "function": {
115
+ "name": "web_search",
116
+ "description": "Search web for info, docs, current events using DuckDuckGo (no API key needed).",
117
+ "parameters": {
118
+ "type": "object",
119
+ "properties": {
120
+ "query": {"type": "string", "description": "Search query to execute"},
121
+ "num_results": {"type": "integer", "description": "Results to return (default: 5, max: 10)"}
122
+ },
123
+ "required": ["query"]
124
+ }
125
+ }
126
+ },
127
+ {
128
+ "type": "function",
129
+ "function": {
130
+ "name": "sub_agent",
131
+ "description": "MANDATORY: MUST CALL THIS FIRST before ANY rg or read_file when answering: 'how something works', architecture, patterns, multi-file flows, or broad exploration. DO NOT search manually - this tool is 10x faster. Examples: 'How does authentication work?', 'Explain the data flow', 'Where is X handled?'",
132
+ "parameters": {
133
+ "type": "object",
134
+ "properties": {
135
+ "query": {"type": "string", "description": "Task query, e.g. 'How does the chat manager handle history?'"}
136
+ },
137
+ "required": ["query"]
138
+ }
139
+ }
140
+ },
141
+ {
142
+ "type": "function",
143
+ "function": {
144
+ "name": "create_task_list",
145
+ "description": "Create or replace an in-session task list for tracking long EDIT workflows.",
146
+ "parameters": {
147
+ "type": "object",
148
+ "properties": {
149
+ "tasks": {"type": "array", "items": {"type": "string"}, "description": "Task descriptions. Non-empty after trimming."},
150
+ "title": {"type": "string", "description": "Optional short title for the task list."}
151
+ },
152
+ "required": ["tasks"]
153
+ }
154
+ }
155
+ },
156
+ {
157
+ "type": "function",
158
+ "function": {
159
+ "name": "complete_task",
160
+ "description": "Mark one or more tasks complete in the current in-session task list.",
161
+ "parameters": {
162
+ "type": "object",
163
+ "properties": {
164
+ "task_id": {"type": "integer", "description": "Zero-based index of a single task to mark complete."},
165
+ "task_ids": {"type": "array", "items": {"type": "integer"}, "description": "Array of zero-based task indices to mark complete."}
166
+ }
167
+ }
168
+ }
169
+ },
170
+ {
171
+ "type": "function",
172
+ "function": {
173
+ "name": "show_task_list",
174
+ "description": "Show the current in-session task list without modifying it.",
175
+ "parameters": {"type": "object", "properties": {}}
176
+ }
177
+ }
178
+ ]
179
+
180
+
181
+ def _tools_for_mode(interaction_mode):
182
+ """Filter tools based on interaction mode.
183
+
184
+ Args:
185
+ interaction_mode: 'plan', 'edit', or 'learn'
186
+
187
+ Returns:
188
+ List of tool definitions suitable for the mode
189
+ """
190
+ if interaction_mode in ("learn", "plan"):
191
+ allowed = {"rg", "read_file", "list_directory", "sub_agent", "web_search"}
192
+ return [tool for tool in TOOLS if tool["function"]["name"] in allowed]
193
+ return TOOLS