File size: 4,735 Bytes
53b2ac9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# SPDX-License-Identifier: MIT
"""
JSON Diff Plugin for TrashClaw

Diff two JSON files or strings showing added, removed, and changed keys.
Handles nested structures with dot-path notation.

Plugin contract:
  TOOL_DEF = dict with name, description, parameters (OpenAI function schema)
  run(**kwargs) -> str  (called with the parsed arguments)
"""

import json
import os

TOOL_DEF = {
    "name": "json_diff",
    "description": "Diff two JSON files or strings. Shows added, removed, and changed keys with dot-path notation for nested structures.",
    "parameters": {
        "type": "object",
        "properties": {
            "file_a": {
                "type": "string",
                "description": "Path to first JSON file (or use json_a for inline JSON)"
            },
            "file_b": {
                "type": "string",
                "description": "Path to second JSON file (or use json_b for inline JSON)"
            },
            "json_a": {
                "type": "string",
                "description": "First JSON string (alternative to file_a)"
            },
            "json_b": {
                "type": "string",
                "description": "Second JSON string (alternative to file_b)"
            }
        },
        "required": []
    }
}


def _flatten(obj, prefix=""):
    """Flatten a nested dict/list into dot-path keys."""
    items = {}
    if isinstance(obj, dict):
        for k, v in obj.items():
            new_key = f"{prefix}.{k}" if prefix else k
            if isinstance(v, (dict, list)):
                items.update(_flatten(v, new_key))
            else:
                items[new_key] = v
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            new_key = f"{prefix}[{i}]"
            if isinstance(v, (dict, list)):
                items.update(_flatten(v, new_key))
            else:
                items[new_key] = v
    else:
        items[prefix] = obj
    return items


def _load_json(file_path: str = "", json_str: str = "", label: str = "input"):
    """Load JSON from file or string. Returns (data, error_msg)."""
    if file_path:
        if not os.path.exists(file_path):
            return None, f"Error: file not found: {file_path}"
        try:
            with open(file_path, "r") as f:
                return json.load(f), None
        except json.JSONDecodeError as e:
            return None, f"Error: invalid JSON in {file_path}: {e}"
        except Exception as e:
            return None, f"Error reading {file_path}: {e}"
    elif json_str:
        try:
            return json.loads(json_str), None
        except json.JSONDecodeError as e:
            return None, f"Error: invalid JSON in {label}: {e}"
    return None, f"Error: no {label} provided (use file or inline JSON)."


def _format_value(v):
    """Format a value for display, truncating long strings."""
    s = json.dumps(v)
    if len(s) > 80:
        s = s[:77] + "..."
    return s


def run(file_a: str = "", file_b: str = "", json_a: str = "", json_b: str = "", **kwargs) -> str:
    data_a, err = _load_json(file_a, json_a, "A")
    if err:
        return err
    data_b, err = _load_json(file_b, json_b, "B")
    if err:
        return err

    flat_a = _flatten(data_a)
    flat_b = _flatten(data_b)

    keys_a = set(flat_a.keys())
    keys_b = set(flat_b.keys())

    added = sorted(keys_b - keys_a)
    removed = sorted(keys_a - keys_b)
    common = sorted(keys_a & keys_b)

    changed = []
    for k in common:
        if flat_a[k] != flat_b[k]:
            changed.append(k)

    if not added and not removed and not changed:
        return "No differences found. The JSON structures are identical."

    lines = ["JSON Diff Results", "=" * 40]

    label_a = file_a or "(inline A)"
    label_b = file_b or "(inline B)"
    lines.append(f"A: {label_a}")
    lines.append(f"B: {label_b}")
    lines.append("")

    if added:
        lines.append(f"ADDED ({len(added)} keys, only in B):")
        for k in added:
            lines.append(f"  + {k}: {_format_value(flat_b[k])}")
        lines.append("")

    if removed:
        lines.append(f"REMOVED ({len(removed)} keys, only in A):")
        for k in removed:
            lines.append(f"  - {k}: {_format_value(flat_a[k])}")
        lines.append("")

    if changed:
        lines.append(f"CHANGED ({len(changed)} keys):")
        for k in changed:
            lines.append(f"  ~ {k}:")
            lines.append(f"      A: {_format_value(flat_a[k])}")
            lines.append(f"      B: {_format_value(flat_b[k])}")
        lines.append("")

    summary = f"Summary: +{len(added)} added, -{len(removed)} removed, ~{len(changed)} changed"
    lines.append(summary)

    return "\n".join(lines)