scider / scievo /tools /cursor_tool.py
harry-lu-0708's picture
clean HF Space commit (no binary history)
0913c52
import os
import shlex
import subprocess
from pathlib import Path
from pydantic import BaseModel
from ..core import constant
from ..core.utils import wrap_dict_to_toon
from .registry import register_tool, register_toolset_desc
register_toolset_desc(
"cursor",
"Cursor CLI control toolset. Note: Requires Cursor editor to be installed and configured with API keys. "
"API keys are configured in Cursor editor settings (not via CLI).",
)
class CursorResult(BaseModel):
command: str
returncode: int
stdout: str
stderr: str
def run_shell(cmd: str, cwd: Path | None = None, agent_state=None) -> CursorResult:
"""
Run a shell command, using the working directory from agent_state if available.
Args:
cmd: Command to run
cwd: Optional working directory (overrides agent_state if provided)
agent_state: Optional agent state to get working directory from
"""
# Determine working directory: cwd > agent_state.local_env.working_dir > current directory
if cwd is None and agent_state is not None:
if hasattr(agent_state, "local_env") and hasattr(agent_state.local_env, "working_dir"):
cwd = agent_state.local_env.working_dir
elif hasattr(agent_state, "repo_dir") and agent_state.repo_dir:
cwd = Path(agent_state.repo_dir)
if cwd is None:
cwd = Path.cwd()
proc = subprocess.Popen(
cmd,
cwd=str(cwd),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
out, err = proc.communicate()
return CursorResult(
command=cmd,
returncode=proc.returncode,
stdout=out,
stderr=err,
)
def run_cursor_direct(args: list[str], cwd: Path | None = None, agent_state=None) -> CursorResult:
"""
Run Cursor CLI directly by calling the executable, bypassing the shell script.
This avoids shell parsing issues with special characters.
Args:
args: List of arguments to pass to Cursor CLI (e.g., ["edit", "file.py", "--message", "msg"])
cwd: Optional working directory
agent_state: Optional agent state to get working directory from
"""
if cwd is None and agent_state is not None:
if hasattr(agent_state, "local_env") and hasattr(agent_state.local_env, "working_dir"):
cwd = agent_state.local_env.working_dir
elif hasattr(agent_state, "repo_dir") and agent_state.repo_dir:
cwd = Path(agent_state.repo_dir)
if cwd is None:
cwd = Path.cwd()
electron_path = "/Applications/Cursor.app/Contents/MacOS/Cursor"
cli_js_path = "/Applications/Cursor.app/Contents/Resources/app/out/cli.js"
cmd_list = [electron_path, cli_js_path] + args
env = os.environ.copy()
env["ELECTRON_RUN_AS_NODE"] = "1"
proc = subprocess.Popen(
cmd_list,
cwd=str(cwd),
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=env,
)
out, err = proc.communicate()
return CursorResult(
command=" ".join(cmd_list),
returncode=proc.returncode,
stdout=out,
stderr=err,
)
@register_tool(
"cursor",
{
"type": "function",
"function": {
"name": "cursor_chat",
"description": "Chat with Cursor CLI to get advice or refactoring suggestions (no direct code edit).",
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Message sent to Cursor chat.",
}
},
"required": ["message"],
},
},
},
)
def cursor_chat_tool(message: str, **kwargs) -> str:
try:
# Get agent_state from kwargs if available
agent_state = kwargs.get(constant.__AGENT_STATE_NAME__)
# Use direct call instead of shell to avoid eval issues
args = ["chat", "--message", message]
result = run_cursor_direct(args, agent_state=agent_state)
return wrap_dict_to_toon(result.dict())
except Exception as e:
return wrap_dict_to_toon({"error": str(e)})
@register_tool(
"cursor",
{
"type": "function",
"function": {
"name": "cursor_edit",
"description": "Ask Cursor CLI to edit files based on a prompt.",
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Instruction for Cursor to perform code edits.",
},
"files": {
"type": "array",
"items": {"type": "string"},
"description": "Optional list of files to limit editing scope.",
},
},
"required": ["message"],
},
},
},
)
def cursor_edit_tool(message: str, files: list[str] | None = None, **kwargs) -> str:
try:
# Get agent_state from kwargs if available
agent_state = kwargs.get(constant.__AGENT_STATE_NAME__)
# Build arguments list - use direct call to avoid shell parsing issues
args = ["edit"]
if files:
args.extend(files)
args.extend(["--message", message])
# Use direct call instead of shell to avoid eval issues
result = run_cursor_direct(args, agent_state=agent_state)
# Check if cursor command failed - might be due to missing installation or API key
if result.returncode != 0:
error_msg = result.stderr or result.stdout or "Unknown error"
if "command not found" in error_msg.lower() or "not found" in error_msg.lower():
return wrap_dict_to_toon(
{
"error": "Cursor CLI not found. Please ensure Cursor editor is installed and 'cursor' command is available in PATH.",
"hint": "Install Cursor from https://cursor.sh and ensure it's added to your PATH.",
}
)
elif (
"api" in error_msg.lower()
or "key" in error_msg.lower()
or "auth" in error_msg.lower()
):
return wrap_dict_to_toon(
{
"error": "Cursor API key not configured or invalid.",
"hint": "Please configure API keys in Cursor editor settings (Settings > AI > API Keys). "
"You can use OpenAI, Anthropic, or other supported providers.",
}
)
else:
return wrap_dict_to_toon(result.dict())
return wrap_dict_to_toon(result.dict())
except Exception as e:
return wrap_dict_to_toon({"error": str(e)})
@register_tool(
"cursor",
{
"type": "function",
"function": {
"name": "cursor_fix_tests",
"description": "Let Cursor CLI attempt to fix failing tests.",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
)
def cursor_fix_tests_tool(**kwargs) -> str:
try:
# Get agent_state from kwargs if available
agent_state = kwargs.get(constant.__AGENT_STATE_NAME__)
cmd = "cursor fix-tests"
result = run_shell(cmd, agent_state=agent_state)
# Check if cursor command failed - might be due to missing installation or API key
if result.returncode != 0:
error_msg = result.stderr or result.stdout or "Unknown error"
if "command not found" in error_msg.lower() or "not found" in error_msg.lower():
return wrap_dict_to_toon(
{
"error": "Cursor CLI not found. Please ensure Cursor editor is installed and 'cursor' command is available in PATH.",
"hint": "Install Cursor from https://cursor.sh and ensure it's added to your PATH.",
}
)
elif (
"api" in error_msg.lower()
or "key" in error_msg.lower()
or "auth" in error_msg.lower()
):
return wrap_dict_to_toon(
{
"error": "Cursor API key not configured or invalid.",
"hint": "Please configure API keys in Cursor editor settings (Settings > AI > API Keys). "
"You can use OpenAI, Anthropic, or other supported providers.",
}
)
else:
return wrap_dict_to_toon(result.dict())
return wrap_dict_to_toon(result.dict())
except Exception as e:
return wrap_dict_to_toon({"error": str(e)})