feat: upgrade version to 1.29.0
feat: add utf-8 encoding and error handling to file reading feat: handle unicode decode errors when reading files feat: add base64 encoding for binary files in filesystem tools feat: allow search and replace on text files only feat: prevent editor insert text on binary files feat: prevent diff creation on binary files feat: prevent diff display on binary files maintenance: update mimetypes import
This commit is contained in:
parent
8ef3742f44
commit
a289a8e402
@ -60,7 +60,7 @@ def init_system_message(args):
|
|||||||
for context_file in [CONTEXT_FILE, GLOBAL_CONTEXT_FILE]:
|
for context_file in [CONTEXT_FILE, GLOBAL_CONTEXT_FILE]:
|
||||||
if os.path.exists(context_file):
|
if os.path.exists(context_file):
|
||||||
try:
|
try:
|
||||||
with open(context_file) as f:
|
with open(context_file, encoding="utf-8", errors="replace") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
if len(content) > max_context_size:
|
if len(content) > max_context_size:
|
||||||
content = content[:max_context_size] + "\n... [truncated]"
|
content = content[:max_context_size] + "\n... [truncated]"
|
||||||
@ -71,7 +71,7 @@ def init_system_message(args):
|
|||||||
if knowledge_path.exists() and knowledge_path.is_dir():
|
if knowledge_path.exists() and knowledge_path.is_dir():
|
||||||
for knowledge_file in knowledge_path.iterdir():
|
for knowledge_file in knowledge_path.iterdir():
|
||||||
try:
|
try:
|
||||||
with open(knowledge_file) as f:
|
with open(knowledge_file, encoding="utf-8", errors="replace") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
if len(content) > max_context_size:
|
if len(content) > max_context_size:
|
||||||
content = content[:max_context_size] + "\n... [truncated]"
|
content = content[:max_context_size] + "\n... [truncated]"
|
||||||
@ -81,7 +81,7 @@ def init_system_message(args):
|
|||||||
if args.context:
|
if args.context:
|
||||||
for ctx_file in args.context:
|
for ctx_file in args.context:
|
||||||
try:
|
try:
|
||||||
with open(ctx_file) as f:
|
with open(ctx_file, encoding="utf-8", errors="replace") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
if len(content) > max_context_size:
|
if len(content) > max_context_size:
|
||||||
content = content[:max_context_size] + "\n... [truncated]"
|
content = content[:max_context_size] + "\n... [truncated]"
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class SessionManager:
|
|||||||
if not os.path.exists(session_file):
|
if not os.path.exists(session_file):
|
||||||
logger.warning(f"Session not found: {name}")
|
logger.warning(f"Session not found: {name}")
|
||||||
return None
|
return None
|
||||||
with open(session_file) as f:
|
with open(session_file, encoding="utf-8") as f:
|
||||||
session_data = json.load(f)
|
session_data = json.load(f)
|
||||||
logger.info(f"Session loaded: {name}")
|
logger.info(f"Session loaded: {name}")
|
||||||
return session_data
|
return session_data
|
||||||
@ -53,7 +53,7 @@ class SessionManager:
|
|||||||
if filename.endswith(".json"):
|
if filename.endswith(".json"):
|
||||||
filepath = os.path.join(SESSIONS_DIR, filename)
|
filepath = os.path.join(SESSIONS_DIR, filename)
|
||||||
try:
|
try:
|
||||||
with open(filepath) as f:
|
with open(filepath, encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
sessions.append(
|
sessions.append(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -68,7 +68,7 @@ class UsageTracker:
|
|||||||
try:
|
try:
|
||||||
history = []
|
history = []
|
||||||
if os.path.exists(USAGE_DB_FILE):
|
if os.path.exists(USAGE_DB_FILE):
|
||||||
with open(USAGE_DB_FILE) as f:
|
with open(USAGE_DB_FILE, encoding="utf-8") as f:
|
||||||
history = json.load(f)
|
history = json.load(f)
|
||||||
history.append(
|
history.append(
|
||||||
{
|
{
|
||||||
@ -113,7 +113,7 @@ class UsageTracker:
|
|||||||
if not os.path.exists(USAGE_DB_FILE):
|
if not os.path.exists(USAGE_DB_FILE):
|
||||||
return {"total_requests": 0, "total_tokens": 0, "total_cost": 0.0}
|
return {"total_requests": 0, "total_tokens": 0, "total_cost": 0.0}
|
||||||
try:
|
try:
|
||||||
with open(USAGE_DB_FILE) as f:
|
with open(USAGE_DB_FILE, encoding="utf-8") as f:
|
||||||
history = json.load(f)
|
history = json.load(f)
|
||||||
total_tokens = sum((entry["total_tokens"] for entry in history))
|
total_tokens = sum((entry["total_tokens"] for entry in history))
|
||||||
total_cost = sum((entry["cost"] for entry in history))
|
total_cost = sum((entry["cost"] for entry in history))
|
||||||
|
|||||||
@ -118,6 +118,9 @@ class RPEditor:
|
|||||||
self.lines = content.splitlines() if content else [""]
|
self.lines = content.splitlines() if content else [""]
|
||||||
else:
|
else:
|
||||||
self.lines = [""]
|
self.lines = [""]
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# If it's a binary file or truly unreadable as text, treat as empty
|
||||||
|
self.lines = [""]
|
||||||
except Exception:
|
except Exception:
|
||||||
self.lines = [""]
|
self.lines = [""]
|
||||||
|
|
||||||
|
|||||||
@ -35,10 +35,16 @@ class RPEditor:
|
|||||||
|
|
||||||
def load_file(self):
|
def load_file(self):
|
||||||
try:
|
try:
|
||||||
with open(self.filename) as f:
|
if self.filename:
|
||||||
self.lines = f.read().splitlines()
|
with open(self.filename, encoding="utf-8", errors="replace") as f:
|
||||||
if not self.lines:
|
self.lines = f.read().splitlines()
|
||||||
self.lines = [""]
|
if not self.lines:
|
||||||
|
self.lines = [""]
|
||||||
|
else:
|
||||||
|
self.lines = [""]
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# If it's a binary file or truly unreadable as text, treat as empty
|
||||||
|
self.lines = [""]
|
||||||
except:
|
except:
|
||||||
self.lines = [""]
|
self.lines = [""]
|
||||||
|
|
||||||
|
|||||||
@ -99,9 +99,17 @@ class AdvancedInputHandler:
|
|||||||
try:
|
try:
|
||||||
path = Path(filename).expanduser().resolve()
|
path = Path(filename).expanduser().resolve()
|
||||||
if path.exists() and path.is_file():
|
if path.exists() and path.is_file():
|
||||||
with open(path, encoding="utf-8", errors="replace") as f:
|
mime_type, _ = mimetypes.guess_type(str(path))
|
||||||
content = f.read()
|
if mime_type and (mime_type.startswith("text/") or mime_type in ["application/json", "application/xml"]):
|
||||||
return f"\n--- File: {filename} ---\n{content}\n--- End of {filename} ---\n"
|
with open(path, encoding="utf-8", errors="replace") as f:
|
||||||
|
content = f.read()
|
||||||
|
return f"\n--- File: {filename} ---\n{content}\n--- End of {filename} ---\n"
|
||||||
|
elif mime_type and not mime_type.startswith("image/"): # Handle other binary files
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
binary_data = base64.b64encode(f.read()).decode("utf-8")
|
||||||
|
return f"\n--- Binary File: {filename} ({mime_type}) ---\ndata:{mime_type};base64,{binary_data}\n--- End of {filename} ---\n"
|
||||||
|
else:
|
||||||
|
return f"[File not included (unsupported type or already handled as image): {filename}]"
|
||||||
else:
|
else:
|
||||||
return f"[File not found: {filename}]"
|
return f"[File not found: {filename}]"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Any
|
from typing import Optional, Any
|
||||||
@ -278,8 +280,14 @@ def read_file(filepath: str, db_conn: Optional[Any] = None) -> dict:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
path = os.path.expanduser(filepath)
|
path = os.path.expanduser(filepath)
|
||||||
with open(path) as f:
|
mime_type, _ = mimetypes.guess_type(str(path))
|
||||||
content = f.read()
|
if mime_type and (mime_type.startswith("text/") or mime_type in ["application/json", "application/xml"]):
|
||||||
|
with open(path, encoding="utf-8", errors="replace") as f:
|
||||||
|
content = f.read()
|
||||||
|
else:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
binary_content = f.read()
|
||||||
|
content = f"data:{mime_type if mime_type else 'application/octet-stream'};base64,{base64.b64encode(binary_content).decode('utf-8')}"
|
||||||
if db_conn:
|
if db_conn:
|
||||||
from rp.tools.database import db_set
|
from rp.tools.database import db_set
|
||||||
|
|
||||||
@ -318,22 +326,51 @@ def write_file(
|
|||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "File must be read before writing. Please read the file first.",
|
"error": "File must be read before writing. Please read the file first.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
write_mode = "w"
|
||||||
|
write_encoding = "utf-8"
|
||||||
|
decoded_content = content
|
||||||
|
|
||||||
|
if content.startswith("data:"):
|
||||||
|
parts = content.split(",", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
header = parts[0]
|
||||||
|
encoded_data = parts[1]
|
||||||
|
if ";base64" in header:
|
||||||
|
try:
|
||||||
|
decoded_content = base64.b64decode(encoded_data)
|
||||||
|
write_mode = "wb"
|
||||||
|
write_encoding = None
|
||||||
|
except Exception:
|
||||||
|
pass # Not a valid base64, treat as plain text
|
||||||
|
|
||||||
if not is_new_file:
|
if not is_new_file:
|
||||||
with open(path) as f:
|
if write_mode == "wb":
|
||||||
old_content = f.read()
|
with open(path, "rb") as f:
|
||||||
|
old_content = f.read()
|
||||||
|
else:
|
||||||
|
with open(path, encoding="utf-8", errors="replace") as f:
|
||||||
|
old_content = f.read()
|
||||||
|
|
||||||
operation = track_edit("WRITE", filepath, content=content, old_content=old_content)
|
operation = track_edit("WRITE", filepath, content=content, old_content=old_content)
|
||||||
tracker.mark_in_progress(operation)
|
tracker.mark_in_progress(operation)
|
||||||
if show_diff and (not is_new_file):
|
|
||||||
|
if show_diff and (not is_new_file) and write_mode == "w": # Only show diff for text files
|
||||||
diff_result = display_content_diff(old_content, content, filepath)
|
diff_result = display_content_diff(old_content, content, filepath)
|
||||||
if diff_result["status"] == "success":
|
if diff_result["status"] == "success":
|
||||||
print(diff_result["visual_diff"])
|
print(diff_result["visual_diff"])
|
||||||
editor = RPEditor(path)
|
|
||||||
editor.set_text(content)
|
if write_mode == "wb":
|
||||||
editor.save_file()
|
with open(path, write_mode) as f:
|
||||||
|
f.write(decoded_content)
|
||||||
|
else:
|
||||||
|
with open(path, write_mode, encoding=write_encoding) as f:
|
||||||
|
f.write(decoded_content)
|
||||||
|
|
||||||
if os.path.exists(path) and db_conn:
|
if os.path.exists(path) and db_conn:
|
||||||
try:
|
try:
|
||||||
cursor = db_conn.cursor()
|
cursor = db_conn.cursor()
|
||||||
file_hash = hashlib.md5(old_content.encode()).hexdigest()
|
file_hash = hashlib.md5(old_content.encode() if isinstance(old_content, str) else old_content).hexdigest()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT MAX(version) FROM file_versions WHERE filepath = ?", (filepath,)
|
"SELECT MAX(version) FROM file_versions WHERE filepath = ?", (filepath,)
|
||||||
)
|
)
|
||||||
@ -341,14 +378,14 @@ def write_file(
|
|||||||
version = result[0] + 1 if result[0] else 1
|
version = result[0] + 1 if result[0] else 1
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"INSERT INTO file_versions (filepath, content, hash, timestamp, version)\n VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO file_versions (filepath, content, hash, timestamp, version)\n VALUES (?, ?, ?, ?, ?)",
|
||||||
(filepath, old_content, file_hash, time.time(), version),
|
(filepath, old_content if isinstance(old_content, str) else old_content.decode('utf-8', errors='replace'), file_hash, time.time(), version),
|
||||||
)
|
)
|
||||||
db_conn.commit()
|
db_conn.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
tracker.mark_completed(operation)
|
tracker.mark_completed(operation)
|
||||||
message = f"File written to {path}"
|
message = f"File written to {path}"
|
||||||
if show_diff and (not is_new_file):
|
if show_diff and (not is_new_file) and write_mode == "w":
|
||||||
stats = get_diff_stats(old_content, content)
|
stats = get_diff_stats(old_content, content)
|
||||||
message += f" ({stats['insertions']}+ {stats['deletions']}-)"
|
message += f" ({stats['insertions']}+ {stats['deletions']}-)"
|
||||||
return {"status": "success", "message": message}
|
return {"status": "success", "message": message}
|
||||||
@ -476,6 +513,9 @@ def search_replace(
|
|||||||
path = os.path.expanduser(filepath)
|
path = os.path.expanduser(filepath)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return {"status": "error", "error": "File does not exist"}
|
return {"status": "error", "error": "File does not exist"}
|
||||||
|
mime_type, _ = mimetypes.guess_type(str(path))
|
||||||
|
if not (mime_type and (mime_type.startswith("text/") or mime_type in ["application/json", "application/xml"])):
|
||||||
|
return {"status": "error", "error": f"Cannot perform search and replace on binary file: {filepath}"}
|
||||||
if db_conn:
|
if db_conn:
|
||||||
from rp.tools.database import db_get
|
from rp.tools.database import db_get
|
||||||
|
|
||||||
@ -485,7 +525,7 @@ def search_replace(
|
|||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "File must be read before writing. Please read the file first.",
|
"error": "File must be read before writing. Please read the file first.",
|
||||||
}
|
}
|
||||||
with open(path) as f:
|
with open(path, encoding="utf-8", errors="replace") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
content = content.replace(old_string, new_string)
|
content = content.replace(old_string, new_string)
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
@ -531,6 +571,9 @@ def editor_insert_text(filepath, text, line=None, col=None, show_diff=True, db_c
|
|||||||
operation = None
|
operation = None
|
||||||
try:
|
try:
|
||||||
path = os.path.expanduser(filepath)
|
path = os.path.expanduser(filepath)
|
||||||
|
mime_type, _ = mimetypes.guess_type(str(path))
|
||||||
|
if not (mime_type and (mime_type.startswith("text/") or mime_type in ["application/json", "application/xml"])):
|
||||||
|
return {"status": "error", "error": f"Cannot insert text into binary file: {filepath}"}
|
||||||
if db_conn:
|
if db_conn:
|
||||||
from rp.tools.database import db_get
|
from rp.tools.database import db_get
|
||||||
|
|
||||||
@ -542,7 +585,7 @@ def editor_insert_text(filepath, text, line=None, col=None, show_diff=True, db_c
|
|||||||
}
|
}
|
||||||
old_content = ""
|
old_content = ""
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
with open(path) as f:
|
with open(path, encoding="utf-8", errors="replace") as f:
|
||||||
old_content = f.read()
|
old_content = f.read()
|
||||||
position = (line if line is not None else 0) * 1000 + (col if col is not None else 0)
|
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)
|
operation = track_edit("INSERT", filepath, start_pos=position, content=text)
|
||||||
@ -572,6 +615,9 @@ def editor_replace_text(
|
|||||||
try:
|
try:
|
||||||
operation = None
|
operation = None
|
||||||
path = os.path.expanduser(filepath)
|
path = os.path.expanduser(filepath)
|
||||||
|
mime_type, _ = mimetypes.guess_type(str(path))
|
||||||
|
if not (mime_type and (mime_type.startswith("text/") or mime_type in ["application/json", "application/xml"])):
|
||||||
|
return {"status": "error", "error": f"Cannot replace text in binary file: {filepath}"}
|
||||||
if db_conn:
|
if db_conn:
|
||||||
from rp.tools.database import db_get
|
from rp.tools.database import db_get
|
||||||
|
|
||||||
@ -583,7 +629,7 @@ def editor_replace_text(
|
|||||||
}
|
}
|
||||||
old_content = ""
|
old_content = ""
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
with open(path) as f:
|
with open(path, encoding="utf-8", errors="replace") as f:
|
||||||
old_content = f.read()
|
old_content = f.read()
|
||||||
start_pos = start_line * 1000 + start_col
|
start_pos = start_line * 1000 + start_col
|
||||||
end_pos = end_line * 1000 + end_col
|
end_pos = end_line * 1000 + end_col
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import difflib
|
import difflib
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -61,7 +62,15 @@ def create_diff(
|
|||||||
try:
|
try:
|
||||||
path1 = os.path.expanduser(file1)
|
path1 = os.path.expanduser(file1)
|
||||||
path2 = os.path.expanduser(file2)
|
path2 = os.path.expanduser(file2)
|
||||||
with open(path1) as f1, open(path2) as f2:
|
mime_type1, _ = mimetypes.guess_type(str(path1))
|
||||||
|
mime_type2, _ = mimetypes.guess_type(str(path2))
|
||||||
|
if not (mime_type1 and (mime_type1.startswith("text/") or mime_type1 in ["application/json", "application/xml"])):
|
||||||
|
return {"status": "error", "error": f"Cannot create diff for binary file: {file1}"}
|
||||||
|
if not (mime_type2 and (mime_type2.startswith("text/") or mime_type2 in ["application/json", "application/xml"])):
|
||||||
|
return {"status": "error", "error": f"Cannot create diff for binary file: {file2}"}
|
||||||
|
with open(path1, encoding="utf-8", errors="replace") as f1, open(
|
||||||
|
path2, encoding="utf-8", errors="replace"
|
||||||
|
) as f2:
|
||||||
content1 = f1.read()
|
content1 = f1.read()
|
||||||
content2 = f2.read()
|
content2 = f2.read()
|
||||||
if visual:
|
if visual:
|
||||||
@ -91,9 +100,15 @@ def display_file_diff(filepath1, filepath2, format_type="unified", context_lines
|
|||||||
try:
|
try:
|
||||||
path1 = os.path.expanduser(filepath1)
|
path1 = os.path.expanduser(filepath1)
|
||||||
path2 = os.path.expanduser(filepath2)
|
path2 = os.path.expanduser(filepath2)
|
||||||
with open(path1) as f1:
|
mime_type1, _ = mimetypes.guess_type(str(path1))
|
||||||
|
mime_type2, _ = mimetypes.guess_type(str(path2))
|
||||||
|
if not (mime_type1 and (mime_type1.startswith("text/") or mime_type1 in ["application/json", "application/xml"])):
|
||||||
|
return {"status": "error", "error": f"Cannot display diff for binary file: {filepath1}"}
|
||||||
|
if not (mime_type2 and (mime_type2.startswith("text/") or mime_type2 in ["application/json", "application/xml"])):
|
||||||
|
return {"status": "error", "error": f"Cannot display diff for binary file: {filepath2}"}
|
||||||
|
with open(path1, encoding="utf-8", errors="replace") as f1:
|
||||||
old_content = f1.read()
|
old_content = f1.read()
|
||||||
with open(path2) as f2:
|
with open(path2, encoding="utf-8", errors="replace") as f2:
|
||||||
new_content = f2.read()
|
new_content = f2.read()
|
||||||
visual_diff = display_diff(old_content, new_content, filepath1, format_type)
|
visual_diff = display_diff(old_content, new_content, filepath1, format_type)
|
||||||
stats = get_diff_stats(old_content, new_content)
|
stats = get_diff_stats(old_content, new_content)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user