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:
Scott Idem
2026-05-12 00:08:36 -04:00
parent 54eef73b74
commit 3c9b8f5909
2 changed files with 67 additions and 1 deletions

View File

@@ -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

View File

@@ -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=(