feat: add file_diff orchestrator tool
Runs diff -u on two project-scoped files. Low risk, no admin required. Covers code review, config comparison, and before/after verification. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ from tools.files import (
|
||||
project_file_list as _project_file_list,
|
||||
file_stat as _file_stat,
|
||||
file_grep as _file_grep,
|
||||
file_diff as _file_diff,
|
||||
file_syntax_check as _file_syntax_check,
|
||||
file_read as _file_read,
|
||||
file_list as _file_list,
|
||||
@@ -108,7 +109,7 @@ import tools.homeassistant as _mod_homeassistant
|
||||
|
||||
TOOL_CATEGORIES: dict[str, list[str]] = {
|
||||
"Web": ["web_search", "http_fetch", "web_read", "http_post"],
|
||||
"Project Files": ["project_file_read", "project_file_list", "file_stat", "file_grep", "file_syntax_check"],
|
||||
"Project Files": ["project_file_read", "project_file_list", "file_stat", "file_grep", "file_diff", "file_syntax_check"],
|
||||
"System Files": ["file_read", "file_list", "file_write", "session_read", "session_search"],
|
||||
"Shell": ["shell_exec", "claude_allow_dir"],
|
||||
"System": ["cortex_restart", "cortex_logs", "cortex_status", "cortex_update"],
|
||||
@@ -151,6 +152,7 @@ _CALLABLES: dict[str, callable] = {
|
||||
"project_file_list": _project_file_list,
|
||||
"file_stat": _file_stat,
|
||||
"file_grep": _file_grep,
|
||||
"file_diff": _file_diff,
|
||||
"file_syntax_check": _file_syntax_check,
|
||||
"file_read": _file_read,
|
||||
"file_list": _file_list,
|
||||
@@ -247,6 +249,7 @@ TOOL_RISK: dict[str, str] = {
|
||||
"project_file_list": "low",
|
||||
"file_stat": "low",
|
||||
"file_grep": "low",
|
||||
"file_diff": "low",
|
||||
"file_syntax_check": "low",
|
||||
|
||||
# System Files — reads beyond project scope are medium; writes are high
|
||||
|
||||
@@ -339,6 +339,45 @@ def _sync_file_grep(path_str: str, pattern: str, context_lines: int, recursive:
|
||||
return header + "\n\n" + "\n\n".join(sections)
|
||||
|
||||
|
||||
async def file_diff(path_a: str, path_b: str) -> str:
|
||||
"""Compare two files and return a unified diff."""
|
||||
return await asyncio.to_thread(_sync_file_diff, path_a, path_b)
|
||||
|
||||
|
||||
def _sync_file_diff(path_a: str, path_b: str) -> str:
|
||||
try:
|
||||
resolved_a = Path(path_a).expanduser().resolve()
|
||||
resolved_b = Path(path_b).expanduser().resolve()
|
||||
except Exception as e:
|
||||
return f"Invalid path: {e}"
|
||||
|
||||
for resolved in (resolved_a, resolved_b):
|
||||
if not _is_project_allowed(resolved):
|
||||
return f"Access denied: {resolved}"
|
||||
if not resolved.exists():
|
||||
return f"File not found: {resolved}"
|
||||
if not resolved.is_file():
|
||||
return f"Not a file: {resolved}"
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["diff", "-u", str(resolved_a), str(resolved_b)],
|
||||
capture_output=True, text=True, timeout=15,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return f"Files are identical: {resolved_a.name} vs {resolved_b.name}"
|
||||
output = result.stdout
|
||||
if not output:
|
||||
return f"diff returned no output (exit {result.returncode}): {result.stderr}"
|
||||
if len(output) > _MAX_BYTES:
|
||||
output = output[:_MAX_BYTES] + "\n… [truncated]"
|
||||
return output
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Timeout running diff"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
async def file_syntax_check(path: str) -> str:
|
||||
"""Check syntax of a Python (.py) or JSON (.json) file."""
|
||||
return await asyncio.to_thread(_sync_file_syntax_check, path)
|
||||
@@ -604,6 +643,30 @@ DECLARATIONS = [
|
||||
required=["path", "pattern"],
|
||||
),
|
||||
),
|
||||
types.FunctionDeclaration(
|
||||
name="file_diff",
|
||||
description=(
|
||||
"Compare two files and return a unified diff (diff -u). "
|
||||
"Use for code review, verifying what changed between two versions of a file, "
|
||||
"or comparing config files side-by-side. "
|
||||
"Returns 'Files are identical' if there are no differences. "
|
||||
"Restricted to the Cortex project directory."
|
||||
),
|
||||
parameters=types.Schema(
|
||||
type=types.Type.OBJECT,
|
||||
properties={
|
||||
"path_a": types.Schema(
|
||||
type=types.Type.STRING,
|
||||
description="Path to the first file (the 'before' or reference file)",
|
||||
),
|
||||
"path_b": types.Schema(
|
||||
type=types.Type.STRING,
|
||||
description="Path to the second file (the 'after' or comparison file)",
|
||||
),
|
||||
},
|
||||
required=["path_a", "path_b"],
|
||||
),
|
||||
),
|
||||
types.FunctionDeclaration(
|
||||
name="file_syntax_check",
|
||||
description=(
|
||||
|
||||
Reference in New Issue
Block a user