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,
|
project_file_list as _project_file_list,
|
||||||
file_stat as _file_stat,
|
file_stat as _file_stat,
|
||||||
file_grep as _file_grep,
|
file_grep as _file_grep,
|
||||||
|
file_diff as _file_diff,
|
||||||
file_syntax_check as _file_syntax_check,
|
file_syntax_check as _file_syntax_check,
|
||||||
file_read as _file_read,
|
file_read as _file_read,
|
||||||
file_list as _file_list,
|
file_list as _file_list,
|
||||||
@@ -108,7 +109,7 @@ import tools.homeassistant as _mod_homeassistant
|
|||||||
|
|
||||||
TOOL_CATEGORIES: dict[str, list[str]] = {
|
TOOL_CATEGORIES: dict[str, list[str]] = {
|
||||||
"Web": ["web_search", "http_fetch", "web_read", "http_post"],
|
"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"],
|
"System Files": ["file_read", "file_list", "file_write", "session_read", "session_search"],
|
||||||
"Shell": ["shell_exec", "claude_allow_dir"],
|
"Shell": ["shell_exec", "claude_allow_dir"],
|
||||||
"System": ["cortex_restart", "cortex_logs", "cortex_status", "cortex_update"],
|
"System": ["cortex_restart", "cortex_logs", "cortex_status", "cortex_update"],
|
||||||
@@ -151,6 +152,7 @@ _CALLABLES: dict[str, callable] = {
|
|||||||
"project_file_list": _project_file_list,
|
"project_file_list": _project_file_list,
|
||||||
"file_stat": _file_stat,
|
"file_stat": _file_stat,
|
||||||
"file_grep": _file_grep,
|
"file_grep": _file_grep,
|
||||||
|
"file_diff": _file_diff,
|
||||||
"file_syntax_check": _file_syntax_check,
|
"file_syntax_check": _file_syntax_check,
|
||||||
"file_read": _file_read,
|
"file_read": _file_read,
|
||||||
"file_list": _file_list,
|
"file_list": _file_list,
|
||||||
@@ -247,6 +249,7 @@ TOOL_RISK: dict[str, str] = {
|
|||||||
"project_file_list": "low",
|
"project_file_list": "low",
|
||||||
"file_stat": "low",
|
"file_stat": "low",
|
||||||
"file_grep": "low",
|
"file_grep": "low",
|
||||||
|
"file_diff": "low",
|
||||||
"file_syntax_check": "low",
|
"file_syntax_check": "low",
|
||||||
|
|
||||||
# System Files — reads beyond project scope are medium; writes are high
|
# 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)
|
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:
|
async def file_syntax_check(path: str) -> str:
|
||||||
"""Check syntax of a Python (.py) or JSON (.json) file."""
|
"""Check syntax of a Python (.py) or JSON (.json) file."""
|
||||||
return await asyncio.to_thread(_sync_file_syntax_check, path)
|
return await asyncio.to_thread(_sync_file_syntax_check, path)
|
||||||
@@ -604,6 +643,30 @@ DECLARATIONS = [
|
|||||||
required=["path", "pattern"],
|
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(
|
types.FunctionDeclaration(
|
||||||
name="file_syntax_check",
|
name="file_syntax_check",
|
||||||
description=(
|
description=(
|
||||||
|
|||||||
Reference in New Issue
Block a user