chore: scaffold project with editorconfig, ci workflows, gitignore, pre-commit, changelog, contributing guide, license, and makefile

This commit is contained in:
2025-11-04 04:17:27 +00:00
commit 5d42e8d377
77 changed files with 10179 additions and 0 deletions
+21
View File
@@ -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'
]
+444
View File
@@ -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": {}
}
}
}
]
+164
View File
@@ -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)}
+47
View File
@@ -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)}
+144
View File
@@ -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)}
+287
View File
@@ -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"}
+91
View File
@@ -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)}
+13
View File
@@ -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()}
+36
View File
@@ -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)