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.
- package/INSTALLATION_METHODS.md +181 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/bin/npm-wrapper.js +171 -0
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +159 -0
- package/package.json +42 -0
- package/requirements.txt +7 -0
- package/scripts/install.js +132 -0
- package/setup.bat +114 -0
- package/setup.sh +135 -0
- package/src/__init__.py +4 -0
- package/src/core/__init__.py +1 -0
- package/src/core/agentic.py +2342 -0
- package/src/core/chat_manager.py +1201 -0
- package/src/core/config_manager.py +269 -0
- package/src/core/init.py +161 -0
- package/src/core/sub_agent.py +174 -0
- package/src/exceptions.py +75 -0
- package/src/llm/__init__.py +1 -0
- package/src/llm/client.py +149 -0
- package/src/llm/config.py +445 -0
- package/src/llm/prompts.py +569 -0
- package/src/llm/providers.py +402 -0
- package/src/llm/token_tracker.py +220 -0
- package/src/ui/__init__.py +1 -0
- package/src/ui/banner.py +103 -0
- package/src/ui/commands.py +489 -0
- package/src/ui/displays.py +167 -0
- package/src/ui/main.py +351 -0
- package/src/ui/prompt_utils.py +162 -0
- package/src/utils/__init__.py +1 -0
- package/src/utils/editor.py +158 -0
- package/src/utils/gitignore_filter.py +149 -0
- package/src/utils/logger.py +254 -0
- package/src/utils/markdown.py +32 -0
- package/src/utils/settings.py +94 -0
- package/src/utils/tools/__init__.py +55 -0
- package/src/utils/tools/command_executor.py +217 -0
- package/src/utils/tools/create_file.py +143 -0
- package/src/utils/tools/definitions.py +193 -0
- package/src/utils/tools/directory.py +374 -0
- package/src/utils/tools/file_editor.py +345 -0
- package/src/utils/tools/file_helpers.py +109 -0
- package/src/utils/tools/file_reader.py +331 -0
- package/src/utils/tools/formatters.py +458 -0
- package/src/utils/tools/parallel_executor.py +195 -0
- package/src/utils/validation.py +117 -0
- package/src/utils/web_search.py +71 -0
- package/vmcode-proxy/.env.example +5 -0
- package/vmcode-proxy/README.md +235 -0
- package/vmcode-proxy/package-lock.json +947 -0
- package/vmcode-proxy/package.json +20 -0
- package/vmcode-proxy/server.js +248 -0
- 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
|