chore: scaffold project with editorconfig, ci workflows, gitignore, pre-commit, changelog, contributing guide, license, and makefile
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
from pr.tools.base import get_tools_definition
|
||||
from pr.tools.filesystem import (
|
||||
read_file, write_file, list_directory, mkdir, chdir, getpwd, index_source_directory, search_replace
|
||||
)
|
||||
from pr.tools.command import run_command, run_command_interactive, tail_process, kill_process
|
||||
from pr.tools.editor import open_editor, editor_insert_text, editor_replace_text, editor_search, close_editor
|
||||
from pr.tools.database import db_set, db_get, db_query
|
||||
from pr.tools.web import http_fetch, web_search, web_search_news
|
||||
from pr.tools.python_exec import python_exec
|
||||
from pr.tools.patch import apply_patch, create_diff
|
||||
|
||||
__all__ = [
|
||||
'get_tools_definition',
|
||||
'read_file', 'write_file', 'list_directory', 'mkdir', 'chdir', 'getpwd', 'index_source_directory', 'search_replace',
|
||||
'open_editor', 'editor_insert_text', 'editor_replace_text', 'editor_search','close_editor',
|
||||
'run_command', 'run_command_interactive',
|
||||
'db_set', 'db_get', 'db_query',
|
||||
'http_fetch', 'web_search', 'web_search_news',
|
||||
'python_exec','tail_process', 'kill_process',
|
||||
'apply_patch', 'create_diff'
|
||||
]
|
||||
@@ -0,0 +1,444 @@
|
||||
def get_tools_definition():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "kill_process",
|
||||
"description": "Terminate a background process by its PID. Use this to stop processes started with run_command that exceeded their timeout.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "The process ID returned by run_command when status is 'running'."
|
||||
}
|
||||
},
|
||||
"required": ["pid"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tail_process",
|
||||
"description": "Monitor and retrieve output from a background process by its PID. Use this to check on processes started with run_command that exceeded their timeout.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "The process ID returned by run_command when status is 'running'."
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Maximum seconds to wait for process completion. Returns partial output if still running.",
|
||||
"default": 30
|
||||
}
|
||||
},
|
||||
"required": ["pid"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "http_fetch",
|
||||
"description": "Fetch content from an HTTP URL",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "The URL to fetch"},
|
||||
"headers": {"type": "object", "description": "Optional HTTP headers"}
|
||||
},
|
||||
"required": ["url"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_command",
|
||||
"description": "Execute a shell command and capture output. Returns immediately after timeout with PID if still running. Use tail_process to monitor or kill_process to terminate long-running commands.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {"type": "string", "description": "The shell command to execute"},
|
||||
"timeout": {"type": "integer", "description": "Maximum seconds to wait for completion", "default": 30}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_command_interactive",
|
||||
"description": "Execute an interactive terminal command that requires user input or displays UI. The command runs in the user's terminal. Returns exit code only.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {"type": "string", "description": "The interactive command to execute (e.g., vim, nano, top)"}
|
||||
},
|
||||
"required": ["command"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read contents of a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {"type": "string", "description": "Path to the file"}
|
||||
},
|
||||
"required": ["filepath"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {"type": "string", "description": "Path to the file"},
|
||||
"content": {"type": "string", "description": "Content to write"}
|
||||
},
|
||||
"required": ["filepath", "content"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_directory",
|
||||
"description": "List directory contents",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Directory path", "default": "."},
|
||||
"recursive": {"type": "boolean", "description": "List recursively", "default": False}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "mkdir",
|
||||
"description": "Create a new directory",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Path of the directory to create"}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "chdir",
|
||||
"description": "Change the current working directory",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Path to change to"}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getpwd",
|
||||
"description": "Get the current working directory",
|
||||
"parameters": {"type": "object", "properties": {}}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "db_set",
|
||||
"description": "Set a key-value pair in the database",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {"type": "string", "description": "The key"},
|
||||
"value": {"type": "string", "description": "The value"}
|
||||
},
|
||||
"required": ["key", "value"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "db_get",
|
||||
"description": "Get a value from the database",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {"type": "string", "description": "The key"}
|
||||
},
|
||||
"required": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "db_query",
|
||||
"description": "Execute a database query",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "SQL query"}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"description": "Perform a web search",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query"}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search_news",
|
||||
"description": "Perform a web search for news",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query for news"}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "python_exec",
|
||||
"description": "Execute Python code",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {"type": "string", "description": "Python code to execute"}
|
||||
},
|
||||
"required": ["code"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "index_source_directory",
|
||||
"description": "Index directory recursively and read all source files.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Path to index"}
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_replace",
|
||||
"description": "Search and replace text in a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {"type": "string", "description": "Path to the file"},
|
||||
"old_string": {"type": "string", "description": "String to replace"},
|
||||
"new_string": {"type": "string", "description": "Replacement string"}
|
||||
},
|
||||
"required": ["filepath", "old_string", "new_string"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "apply_patch",
|
||||
"description": "Apply a patch to a file, especially for source code",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {"type": "string", "description": "Path to the file to patch"},
|
||||
"patch_content": {"type": "string", "description": "The patch content as a string"}
|
||||
},
|
||||
"required": ["filepath", "patch_content"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_diff",
|
||||
"description": "Create a unified diff between two files",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file1": {"type": "string", "description": "Path to the first file"},
|
||||
"file2": {"type": "string", "description": "Path to the second file"},
|
||||
"fromfile": {"type": "string", "description": "Label for the first file", "default": "file1"},
|
||||
"tofile": {"type": "string", "description": "Label for the second file", "default": "file2"}
|
||||
},
|
||||
"required": ["file1", "file2"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "open_editor",
|
||||
"description": "Open the RPEditor for a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {"type": "string", "description": "Path to the file"}
|
||||
},
|
||||
"required": ["filepath"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "close_editor",
|
||||
"description": "Close the RPEditor. Always close files when finished editing.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {"type": "string", "description": "Path to the file"}
|
||||
},
|
||||
"required": ["filepath"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "editor_insert_text",
|
||||
"description": "Insert text at cursor position in the editor",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {"type": "string", "description": "Path to the file"},
|
||||
"text": {"type": "string", "description": "Text to insert"},
|
||||
"line": {"type": "integer", "description": "Line number (optional)"},
|
||||
"col": {"type": "integer", "description": "Column number (optional)"}
|
||||
},
|
||||
"required": ["filepath", "text"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "editor_replace_text",
|
||||
"description": "Replace text in a range",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {"type": "string", "description": "Path to the file"},
|
||||
"start_line": {"type": "integer", "description": "Start line"},
|
||||
"start_col": {"type": "integer", "description": "Start column"},
|
||||
"end_line": {"type": "integer", "description": "End line"},
|
||||
"end_col": {"type": "integer", "description": "End column"},
|
||||
"new_text": {"type": "string", "description": "New text"}
|
||||
},
|
||||
"required": ["filepath", "start_line", "start_col", "end_line", "end_col", "new_text"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "editor_search",
|
||||
"description": "Search for a pattern in the file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {"type": "string", "description": "Path to the file"},
|
||||
"pattern": {"type": "string", "description": "Regex pattern"},
|
||||
"start_line": {"type": "integer", "description": "Start line", "default": 0}
|
||||
},
|
||||
"required": ["filepath", "pattern"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "display_file_diff",
|
||||
"description": "Display a visual colored diff between two files with syntax highlighting and statistics",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath1": {"type": "string", "description": "Path to the original file"},
|
||||
"filepath2": {"type": "string", "description": "Path to the modified file"},
|
||||
"format_type": {"type": "string", "description": "Display format: 'unified' or 'side-by-side'", "default": "unified"}
|
||||
},
|
||||
"required": ["filepath1", "filepath2"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "display_edit_summary",
|
||||
"description": "Display a summary of all edit operations performed during the session",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "display_edit_timeline",
|
||||
"description": "Display a timeline of all edit operations with details",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"show_content": {"type": "boolean", "description": "Show content previews", "default": False}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "clear_edit_tracker",
|
||||
"description": "Clear the edit tracker to start fresh",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import select
|
||||
from pr.multiplexer import create_multiplexer, close_multiplexer, get_multiplexer
|
||||
|
||||
_processes = {}
|
||||
|
||||
def _register_process(pid:int, process):
|
||||
_processes[pid] = process
|
||||
return _processes
|
||||
|
||||
def _get_process(pid:int):
|
||||
return _processes.get(pid)
|
||||
|
||||
def kill_process(pid:int):
|
||||
try:
|
||||
process = _get_process(pid)
|
||||
if process:
|
||||
process.kill()
|
||||
_processes.pop(pid)
|
||||
|
||||
mux_name = f"cmd-{pid}"
|
||||
if get_multiplexer(mux_name):
|
||||
close_multiplexer(mux_name)
|
||||
|
||||
return {"status": "success", "message": f"Process {pid} has been killed"}
|
||||
else:
|
||||
return {"status": "error", "error": f"Process {pid} not found"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def tail_process(pid: int, timeout: int = 30):
|
||||
process = _get_process(pid)
|
||||
if process:
|
||||
mux_name = f"cmd-{pid}"
|
||||
mux = get_multiplexer(mux_name)
|
||||
|
||||
if not mux:
|
||||
mux_name, mux = create_multiplexer(mux_name, show_output=True)
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
timeout_duration = timeout
|
||||
stdout_content = ""
|
||||
stderr_content = ""
|
||||
|
||||
while True:
|
||||
if process.poll() is not None:
|
||||
remaining_stdout, remaining_stderr = process.communicate()
|
||||
if remaining_stdout:
|
||||
mux.write_stdout(remaining_stdout)
|
||||
stdout_content += remaining_stdout
|
||||
if remaining_stderr:
|
||||
mux.write_stderr(remaining_stderr)
|
||||
stderr_content += remaining_stderr
|
||||
|
||||
if pid in _processes:
|
||||
_processes.pop(pid)
|
||||
|
||||
close_multiplexer(mux_name)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"stdout": stdout_content,
|
||||
"stderr": stderr_content,
|
||||
"returncode": process.returncode
|
||||
}
|
||||
|
||||
if time.time() - start_time > timeout_duration:
|
||||
return {
|
||||
"status": "running",
|
||||
"message": "Process is still running. Call tail_process again to continue monitoring.",
|
||||
"stdout_so_far": stdout_content,
|
||||
"stderr_so_far": stderr_content,
|
||||
"pid": pid
|
||||
}
|
||||
|
||||
ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
|
||||
for pipe in ready:
|
||||
if pipe == process.stdout:
|
||||
line = process.stdout.readline()
|
||||
if line:
|
||||
mux.write_stdout(line)
|
||||
stdout_content += line
|
||||
elif pipe == process.stderr:
|
||||
line = process.stderr.readline()
|
||||
if line:
|
||||
mux.write_stderr(line)
|
||||
stderr_content += line
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
else:
|
||||
return {"status": "error", "error": f"Process {pid} not found"}
|
||||
|
||||
|
||||
def run_command(command, timeout=30):
|
||||
mux_name = None
|
||||
try:
|
||||
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
_register_process(process.pid, process)
|
||||
|
||||
mux_name, mux = create_multiplexer(f"cmd-{process.pid}", show_output=True)
|
||||
|
||||
start_time = time.time()
|
||||
timeout_duration = timeout
|
||||
stdout_content = ""
|
||||
stderr_content = ""
|
||||
|
||||
while True:
|
||||
if process.poll() is not None:
|
||||
remaining_stdout, remaining_stderr = process.communicate()
|
||||
if remaining_stdout:
|
||||
mux.write_stdout(remaining_stdout)
|
||||
stdout_content += remaining_stdout
|
||||
if remaining_stderr:
|
||||
mux.write_stderr(remaining_stderr)
|
||||
stderr_content += remaining_stderr
|
||||
|
||||
if process.pid in _processes:
|
||||
_processes.pop(process.pid)
|
||||
|
||||
close_multiplexer(mux_name)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"stdout": stdout_content,
|
||||
"stderr": stderr_content,
|
||||
"returncode": process.returncode
|
||||
}
|
||||
|
||||
if time.time() - start_time > timeout_duration:
|
||||
return {
|
||||
"status": "running",
|
||||
"message": f"Process still running after {timeout}s timeout. Use tail_process({process.pid}) to monitor or kill_process({process.pid}) to terminate.",
|
||||
"stdout_so_far": stdout_content,
|
||||
"stderr_so_far": stderr_content,
|
||||
"pid": process.pid,
|
||||
"mux_name": mux_name
|
||||
}
|
||||
|
||||
ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
|
||||
for pipe in ready:
|
||||
if pipe == process.stdout:
|
||||
line = process.stdout.readline()
|
||||
if line:
|
||||
mux.write_stdout(line)
|
||||
stdout_content += line
|
||||
elif pipe == process.stderr:
|
||||
line = process.stderr.readline()
|
||||
if line:
|
||||
mux.write_stderr(line)
|
||||
stderr_content += line
|
||||
except Exception as e:
|
||||
if mux_name:
|
||||
close_multiplexer(mux_name)
|
||||
return {"status": "error", "error": str(e)}
|
||||
def run_command_interactive(command):
|
||||
try:
|
||||
return_code = os.system(command)
|
||||
return {"status": "success", "returncode": return_code}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
@@ -0,0 +1,47 @@
|
||||
import time
|
||||
|
||||
def db_set(key, value, db_conn):
|
||||
if not db_conn:
|
||||
return {"status": "error", "error": "Database not initialized"}
|
||||
|
||||
try:
|
||||
cursor = db_conn.cursor()
|
||||
cursor.execute("""INSERT OR REPLACE INTO kv_store (key, value, timestamp)
|
||||
VALUES (?, ?, ?)""", (key, value, time.time()))
|
||||
db_conn.commit()
|
||||
return {"status": "success", "message": f"Set {key}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def db_get(key, db_conn):
|
||||
if not db_conn:
|
||||
return {"status": "error", "error": "Database not initialized"}
|
||||
|
||||
try:
|
||||
cursor = db_conn.cursor()
|
||||
cursor.execute("SELECT value FROM kv_store WHERE key = ?", (key,))
|
||||
result = cursor.fetchone()
|
||||
if result:
|
||||
return {"status": "success", "value": result[0]}
|
||||
else:
|
||||
return {"status": "error", "error": "Key not found"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def db_query(query, db_conn):
|
||||
if not db_conn:
|
||||
return {"status": "error", "error": "Database not initialized"}
|
||||
|
||||
try:
|
||||
cursor = db_conn.cursor()
|
||||
cursor.execute(query)
|
||||
|
||||
if query.strip().upper().startswith('SELECT'):
|
||||
results = cursor.fetchall()
|
||||
columns = [desc[0] for desc in cursor.description] if cursor.description else []
|
||||
return {"status": "success", "columns": columns, "rows": results}
|
||||
else:
|
||||
db_conn.commit()
|
||||
return {"status": "success", "rows_affected": cursor.rowcount}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
@@ -0,0 +1,144 @@
|
||||
from pr.editor import RPEditor
|
||||
from pr.multiplexer import create_multiplexer, close_multiplexer, get_multiplexer
|
||||
from ..ui.diff_display import display_diff, get_diff_stats
|
||||
from ..ui.edit_feedback import track_edit, tracker
|
||||
from ..tools.patch import display_content_diff
|
||||
import os
|
||||
import os.path
|
||||
|
||||
_editors = {}
|
||||
|
||||
def get_editor(filepath):
|
||||
if filepath not in _editors:
|
||||
_editors[filepath] = RPEditor(filepath)
|
||||
return _editors[filepath]
|
||||
|
||||
def close_editor(filepath):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
editor = get_editor(path)
|
||||
editor.close()
|
||||
|
||||
mux_name = f"editor-{path}"
|
||||
mux = get_multiplexer(mux_name)
|
||||
if mux:
|
||||
mux.write_stdout(f"Closed editor for: {path}\n")
|
||||
close_multiplexer(mux_name)
|
||||
|
||||
return {"status": "success", "message": f"Editor closed for {path}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def open_editor(filepath):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
editor = RPEditor(path)
|
||||
editor.start()
|
||||
|
||||
mux_name = f"editor-{path}"
|
||||
mux_name, mux = create_multiplexer(mux_name, show_output=True)
|
||||
mux.write_stdout(f"Opened editor for: {path}\n")
|
||||
|
||||
return {"status": "success", "message": f"Editor opened for {path}", "mux_name": mux_name}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def editor_insert_text(filepath, text, line=None, col=None, show_diff=True):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
|
||||
old_content = ""
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
old_content = f.read()
|
||||
|
||||
position = (line if line is not None else 0) * 1000 + (col if col is not None else 0)
|
||||
operation = track_edit('INSERT', filepath, start_pos=position, content=text)
|
||||
tracker.mark_in_progress(operation)
|
||||
|
||||
editor = get_editor(path)
|
||||
if line is not None and col is not None:
|
||||
editor.move_cursor_to(line, col)
|
||||
editor.insert_text(text)
|
||||
editor.save_file()
|
||||
|
||||
mux_name = f"editor-{path}"
|
||||
mux = get_multiplexer(mux_name)
|
||||
if mux:
|
||||
location = f" at line {line}, col {col}" if line is not None and col is not None else ""
|
||||
preview = text[:50] + "..." if len(text) > 50 else text
|
||||
mux.write_stdout(f"Inserted text{location}: {repr(preview)}\n")
|
||||
|
||||
if show_diff and old_content:
|
||||
with open(path, 'r') as f:
|
||||
new_content = f.read()
|
||||
diff_result = display_content_diff(old_content, new_content, filepath)
|
||||
if diff_result["status"] == "success":
|
||||
mux.write_stdout(diff_result["visual_diff"] + "\n")
|
||||
|
||||
tracker.mark_completed(operation)
|
||||
result = {"status": "success", "message": f"Inserted text in {path}"}
|
||||
close_editor(filepath)
|
||||
return result
|
||||
except Exception as e:
|
||||
if 'operation' in locals():
|
||||
tracker.mark_failed(operation)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def editor_replace_text(filepath, start_line, start_col, end_line, end_col, new_text, show_diff=True):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
|
||||
old_content = ""
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
old_content = f.read()
|
||||
|
||||
start_pos = start_line * 1000 + start_col
|
||||
end_pos = end_line * 1000 + end_col
|
||||
operation = track_edit('REPLACE', filepath, start_pos=start_pos, end_pos=end_pos,
|
||||
content=new_text, old_content=old_content)
|
||||
tracker.mark_in_progress(operation)
|
||||
|
||||
editor = get_editor(path)
|
||||
editor.replace_text(start_line, start_col, end_line, end_col, new_text)
|
||||
editor.save_file()
|
||||
|
||||
mux_name = f"editor-{path}"
|
||||
mux = get_multiplexer(mux_name)
|
||||
if mux:
|
||||
preview = new_text[:50] + "..." if len(new_text) > 50 else new_text
|
||||
mux.write_stdout(f"Replaced text from ({start_line},{start_col}) to ({end_line},{end_col}): {repr(preview)}\n")
|
||||
|
||||
if show_diff and old_content:
|
||||
with open(path, 'r') as f:
|
||||
new_content = f.read()
|
||||
diff_result = display_content_diff(old_content, new_content, filepath)
|
||||
if diff_result["status"] == "success":
|
||||
mux.write_stdout(diff_result["visual_diff"] + "\n")
|
||||
|
||||
tracker.mark_completed(operation)
|
||||
result = {"status": "success", "message": f"Replaced text in {path}"}
|
||||
close_editor(filepath)
|
||||
return result
|
||||
except Exception as e:
|
||||
if 'operation' in locals():
|
||||
tracker.mark_failed(operation)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def editor_search(filepath, pattern, start_line=0):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
editor = RPEditor(path)
|
||||
results = editor.search(pattern, start_line)
|
||||
|
||||
mux_name = f"editor-{path}"
|
||||
mux = get_multiplexer(mux_name)
|
||||
if mux:
|
||||
mux.write_stdout(f"Searched for pattern '{pattern}' from line {start_line}: {len(results)} matches\n")
|
||||
|
||||
result = {"status": "success", "results": results}
|
||||
close_editor(filepath)
|
||||
return result
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
@@ -0,0 +1,287 @@
|
||||
import os
|
||||
import hashlib
|
||||
import time
|
||||
from typing import Dict
|
||||
from pr.editor import RPEditor
|
||||
from ..ui.diff_display import display_diff, get_diff_stats
|
||||
from ..ui.edit_feedback import track_edit, tracker
|
||||
from ..tools.patch import display_content_diff
|
||||
|
||||
_id = 0
|
||||
|
||||
def get_uid():
|
||||
global _id
|
||||
_id += 3
|
||||
return _id
|
||||
|
||||
def read_file(filepath, db_conn=None):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
with open(path, 'r') as f:
|
||||
content = f.read()
|
||||
if db_conn:
|
||||
from pr.tools.database import db_set
|
||||
db_set("read:" + path, "true", db_conn)
|
||||
return {"status": "success", "content": content}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def write_file(filepath, content, db_conn=None, show_diff=True):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
old_content = ""
|
||||
is_new_file = not os.path.exists(path)
|
||||
|
||||
if not is_new_file and db_conn:
|
||||
from pr.tools.database import db_get
|
||||
read_status = db_get("read:" + path, db_conn)
|
||||
if read_status.get("status") != "success" or read_status.get("value") != "true":
|
||||
return {"status": "error", "error": "File must be read before writing. Please read the file first."}
|
||||
|
||||
if not is_new_file:
|
||||
with open(path, 'r') as f:
|
||||
old_content = f.read()
|
||||
|
||||
operation = track_edit('WRITE', filepath, content=content, old_content=old_content)
|
||||
tracker.mark_in_progress(operation)
|
||||
|
||||
if show_diff and not is_new_file:
|
||||
diff_result = display_content_diff(old_content, content, filepath)
|
||||
if diff_result["status"] == "success":
|
||||
print(diff_result["visual_diff"])
|
||||
|
||||
editor = RPEditor(path)
|
||||
editor.set_text(content)
|
||||
editor.save_file()
|
||||
|
||||
if os.path.exists(path) and db_conn:
|
||||
try:
|
||||
cursor = db_conn.cursor()
|
||||
file_hash = hashlib.md5(old_content.encode()).hexdigest()
|
||||
|
||||
cursor.execute("SELECT MAX(version) FROM file_versions WHERE filepath = ?", (filepath,))
|
||||
result = cursor.fetchone()
|
||||
version = (result[0] + 1) if result[0] else 1
|
||||
|
||||
cursor.execute("""INSERT INTO file_versions (filepath, content, hash, timestamp, version)
|
||||
VALUES (?, ?, ?, ?, ?)""",
|
||||
(filepath, old_content, file_hash, time.time(), version))
|
||||
db_conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
tracker.mark_completed(operation)
|
||||
|
||||
message = f"File written to {path}"
|
||||
if show_diff and not is_new_file:
|
||||
stats = get_diff_stats(old_content, content)
|
||||
message += f" ({stats['insertions']}+ {stats['deletions']}-)"
|
||||
|
||||
return {"status": "success", "message": message}
|
||||
except Exception as e:
|
||||
if 'operation' in locals():
|
||||
tracker.mark_failed(operation)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def list_directory(path=".", recursive=False):
|
||||
try:
|
||||
path = os.path.expanduser(path)
|
||||
items = []
|
||||
if recursive:
|
||||
for root, dirs, files in os.walk(path):
|
||||
for name in files:
|
||||
item_path = os.path.join(root, name)
|
||||
items.append({"path": item_path, "type": "file", "size": os.path.getsize(item_path)})
|
||||
for name in dirs:
|
||||
items.append({"path": os.path.join(root, name), "type": "directory"})
|
||||
else:
|
||||
for item in os.listdir(path):
|
||||
item_path = os.path.join(path, item)
|
||||
items.append({
|
||||
"name": item,
|
||||
"type": "directory" if os.path.isdir(item_path) else "file",
|
||||
"size": os.path.getsize(item_path) if os.path.isfile(item_path) else None
|
||||
})
|
||||
return {"status": "success", "items": items}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def mkdir(path):
|
||||
try:
|
||||
os.makedirs(os.path.expanduser(path), exist_ok=True)
|
||||
return {"status": "success", "message": f"Directory created at {path}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def chdir(path):
|
||||
try:
|
||||
os.chdir(os.path.expanduser(path))
|
||||
return {"status": "success", "new_path": os.getcwd()}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def getpwd():
|
||||
try:
|
||||
return {"status": "success", "path": os.getcwd()}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def index_source_directory(path):
|
||||
extensions = [
|
||||
".py", ".js", ".ts", ".java", ".cpp", ".c", ".h", ".hpp",
|
||||
".html", ".css", ".json", ".xml", ".md", ".sh", ".rb", ".go"
|
||||
]
|
||||
source_files = []
|
||||
try:
|
||||
for root, _, files in os.walk(os.path.expanduser(path)):
|
||||
for file in files:
|
||||
if any(file.endswith(ext) for ext in extensions):
|
||||
filepath = os.path.join(root, file)
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
source_files.append({
|
||||
"path": filepath,
|
||||
"content": content
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
return {"status": "success", "indexed_files": source_files}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def search_replace(filepath, old_string, new_string, db_conn=None):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
if not os.path.exists(path):
|
||||
return {"status": "error", "error": "File does not exist"}
|
||||
if db_conn:
|
||||
from pr.tools.database import db_get
|
||||
read_status = db_get("read:" + path, db_conn)
|
||||
if read_status.get("status") != "success" or read_status.get("value") != "true":
|
||||
return {"status": "error", "error": "File must be read before writing. Please read the file first."}
|
||||
with open(path, 'r') as f:
|
||||
content = f.read()
|
||||
content = content.replace(old_string, new_string)
|
||||
with open(path, 'w') as f:
|
||||
f.write(content)
|
||||
return {"status": "success", "message": f"Replaced '{old_string}' with '{new_string}' in {path}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
_editors = {}
|
||||
|
||||
def get_editor(filepath):
|
||||
if filepath not in _editors:
|
||||
_editors[filepath] = RPEditor(filepath)
|
||||
return _editors[filepath]
|
||||
|
||||
def close_editor(filepath):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
editor = get_editor(path)
|
||||
editor.close()
|
||||
return {"status": "success", "message": f"Editor closed for {path}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def open_editor(filepath):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
editor = RPEditor(path)
|
||||
editor.start()
|
||||
return {"status": "success", "message": f"Editor opened for {path}"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def editor_insert_text(filepath, text, line=None, col=None, show_diff=True, db_conn=None):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
if db_conn:
|
||||
from pr.tools.database import db_get
|
||||
read_status = db_get("read:" + path, db_conn)
|
||||
if read_status.get("status") != "success" or read_status.get("value") != "true":
|
||||
return {"status": "error", "error": "File must be read before writing. Please read the file first."}
|
||||
|
||||
old_content = ""
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
old_content = f.read()
|
||||
|
||||
position = (line if line is not None else 0) * 1000 + (col if col is not None else 0)
|
||||
operation = track_edit('INSERT', filepath, start_pos=position, content=text)
|
||||
tracker.mark_in_progress(operation)
|
||||
|
||||
editor = get_editor(path)
|
||||
if line is not None and col is not None:
|
||||
editor.move_cursor_to(line, col)
|
||||
editor.insert_text(text)
|
||||
editor.save_file()
|
||||
|
||||
if show_diff and old_content:
|
||||
with open(path, 'r') as f:
|
||||
new_content = f.read()
|
||||
diff_result = display_content_diff(old_content, new_content, filepath)
|
||||
if diff_result["status"] == "success":
|
||||
print(diff_result["visual_diff"])
|
||||
|
||||
tracker.mark_completed(operation)
|
||||
return {"status": "success", "message": f"Inserted text in {path}"}
|
||||
except Exception as e:
|
||||
if 'operation' in locals():
|
||||
tracker.mark_failed(operation)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def editor_replace_text(filepath, start_line, start_col, end_line, end_col, new_text, show_diff=True, db_conn=None):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
if db_conn:
|
||||
from pr.tools.database import db_get
|
||||
read_status = db_get("read:" + path, db_conn)
|
||||
if read_status.get("status") != "success" or read_status.get("value") != "true":
|
||||
return {"status": "error", "error": "File must be read before writing. Please read the file first."}
|
||||
|
||||
old_content = ""
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
old_content = f.read()
|
||||
|
||||
start_pos = start_line * 1000 + start_col
|
||||
end_pos = end_line * 1000 + end_col
|
||||
operation = track_edit('REPLACE', filepath, start_pos=start_pos, end_pos=end_pos,
|
||||
content=new_text, old_content=old_content)
|
||||
tracker.mark_in_progress(operation)
|
||||
|
||||
editor = get_editor(path)
|
||||
editor.replace_text(start_line, start_col, end_line, end_col, new_text)
|
||||
editor.save_file()
|
||||
|
||||
if show_diff and old_content:
|
||||
with open(path, 'r') as f:
|
||||
new_content = f.read()
|
||||
diff_result = display_content_diff(old_content, new_content, filepath)
|
||||
if diff_result["status"] == "success":
|
||||
print(diff_result["visual_diff"])
|
||||
|
||||
tracker.mark_completed(operation)
|
||||
return {"status": "success", "message": f"Replaced text in {path}"}
|
||||
except Exception as e:
|
||||
if 'operation' in locals():
|
||||
tracker.mark_failed(operation)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def display_edit_summary():
|
||||
from ..ui.edit_feedback import display_edit_summary
|
||||
return display_edit_summary()
|
||||
|
||||
|
||||
def display_edit_timeline(show_content=False):
|
||||
from ..ui.edit_feedback import display_edit_timeline
|
||||
return display_edit_timeline(show_content)
|
||||
|
||||
|
||||
def clear_edit_tracker():
|
||||
from ..ui.edit_feedback import clear_tracker
|
||||
clear_tracker()
|
||||
return {"status": "success", "message": "Edit tracker cleared"}
|
||||
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
import difflib
|
||||
from ..ui.diff_display import display_diff, get_diff_stats, DiffDisplay
|
||||
|
||||
def apply_patch(filepath, patch_content, db_conn=None):
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
if db_conn:
|
||||
from pr.tools.database import db_get
|
||||
read_status = db_get("read:" + path, db_conn)
|
||||
if read_status.get("status") != "success" or read_status.get("value") != "true":
|
||||
return {"status": "error", "error": "File must be read before writing. Please read the file first."}
|
||||
# Write patch to temp file
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.patch') as f:
|
||||
f.write(patch_content)
|
||||
patch_file = f.name
|
||||
# Run patch command
|
||||
result = subprocess.run(['patch', path, patch_file], capture_output=True, text=True, cwd=os.path.dirname(path))
|
||||
os.unlink(patch_file)
|
||||
if result.returncode == 0:
|
||||
return {"status": "success", "output": result.stdout.strip()}
|
||||
else:
|
||||
return {"status": "error", "error": result.stderr.strip()}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def create_diff(file1, file2, fromfile='file1', tofile='file2', visual=False, format_type='unified'):
|
||||
try:
|
||||
path1 = os.path.expanduser(file1)
|
||||
path2 = os.path.expanduser(file2)
|
||||
with open(path1, 'r') as f1, open(path2, 'r') as f2:
|
||||
content1 = f1.read()
|
||||
content2 = f2.read()
|
||||
|
||||
if visual:
|
||||
visual_diff = display_diff(content1, content2, fromfile, format_type)
|
||||
stats = get_diff_stats(content1, content2)
|
||||
lines1 = content1.splitlines(keepends=True)
|
||||
lines2 = content2.splitlines(keepends=True)
|
||||
plain_diff = list(difflib.unified_diff(lines1, lines2, fromfile=fromfile, tofile=tofile))
|
||||
return {
|
||||
"status": "success",
|
||||
"diff": ''.join(plain_diff),
|
||||
"visual_diff": visual_diff,
|
||||
"stats": stats
|
||||
}
|
||||
else:
|
||||
lines1 = content1.splitlines(keepends=True)
|
||||
lines2 = content2.splitlines(keepends=True)
|
||||
diff = list(difflib.unified_diff(lines1, lines2, fromfile=fromfile, tofile=tofile))
|
||||
return {"status": "success", "diff": ''.join(diff)}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def display_file_diff(filepath1, filepath2, format_type='unified', context_lines=3):
|
||||
try:
|
||||
path1 = os.path.expanduser(filepath1)
|
||||
path2 = os.path.expanduser(filepath2)
|
||||
|
||||
with open(path1, 'r') as f1:
|
||||
old_content = f1.read()
|
||||
with open(path2, 'r') as f2:
|
||||
new_content = f2.read()
|
||||
|
||||
visual_diff = display_diff(old_content, new_content, filepath1, format_type)
|
||||
stats = get_diff_stats(old_content, new_content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"visual_diff": visual_diff,
|
||||
"stats": stats
|
||||
}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def display_content_diff(old_content, new_content, filename='file', format_type='unified'):
|
||||
try:
|
||||
visual_diff = display_diff(old_content, new_content, filename, format_type)
|
||||
stats = get_diff_stats(old_content, new_content)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"visual_diff": visual_diff,
|
||||
"stats": stats
|
||||
}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
@@ -0,0 +1,13 @@
|
||||
import traceback
|
||||
from io import StringIO
|
||||
import contextlib
|
||||
|
||||
def python_exec(code, python_globals):
|
||||
try:
|
||||
output = StringIO()
|
||||
with contextlib.redirect_stdout(output):
|
||||
exec(code, python_globals)
|
||||
|
||||
return {"status": "success", "output": output.getvalue()}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e), "traceback": traceback.format_exc()}
|
||||
@@ -0,0 +1,36 @@
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
import json
|
||||
|
||||
def http_fetch(url, headers=None):
|
||||
try:
|
||||
req = urllib.request.Request(url)
|
||||
if headers:
|
||||
for key, value in headers.items():
|
||||
req.add_header(key, value)
|
||||
|
||||
with urllib.request.urlopen(req) as response:
|
||||
content = response.read().decode('utf-8')
|
||||
return {"status": "success", "content": content[:10000]}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def _perform_search(base_url, query, params=None):
|
||||
try:
|
||||
full_url = f"https://static.molodetz.nl/search.cgi?query={query}"
|
||||
|
||||
with urllib.request.urlopen(full_url) as response:
|
||||
content = response.read().decode('utf-8')
|
||||
return {"status": "success", "content": json.loads(content)}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def web_search(query):
|
||||
base_url = "https://search.molodetz.nl/search"
|
||||
return _perform_search(base_url, query)
|
||||
|
||||
def web_search_news(query):
|
||||
base_url = "https://search.molodetz.nl/search"
|
||||
return _perform_search(base_url, query)
|
||||
|
||||
Reference in New Issue
Block a user