Metadata-Version: 2.3
Name: uni-diff-patch
Version: 0.1.3
Summary: MCP server and CLI tool for generating valid unified diff patches deterministically
Author: LimLLL
Author-email: LimLLL <github@awebapp.useforall.com>
Requires-Dist: mcp[cli]>=1.28,<2.0
Requires-Dist: click
Requires-Dist: pydantic>=2.13.4
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# uni-diff-patch

MCP server + CLI tool for generating and maintaining valid unified diff patches deterministically.

LLM provides structured file changes, the tool computes correct diffs with the Python standard library, and output patch files can be inspected, appended, upserted, overwritten, created, or pruned entry-by-entry without tool-specific metadata in the `.diff` file.

---

MCP 服务器 + CLI 工具，确定性生成并维护合法的 unified diff patch。

LLM 只需提供结构化文件变更，工具通过 Python 标准库计算正确 diff，并支持对已有 patch 文件做条目级 inspect / append / upsert / overwrite / create_only / remove。生成的 `.diff` 文件不包含工具私有元数据。

## Install / 安装

```bash
uv add uni-diff-patch      # as dependency / 作为依赖
uvx uni-diff-patch --help  # run directly / 直接运行
```

## CLI / 命令行

```bash
# Generate patch from JSON spec / 从 JSON spec 生成 patch
uni-diff-patch generate --spec changes.json -o output.diff

# Upsert entries in an existing patch / 更新已有 patch 中的条目
uni-diff-patch generate --spec changes.json -o output.diff --write-mode upsert

# Generate with project_root_dir for absolute paths / 指定项目根目录（用于绝对路径）
uni-diff-patch generate --spec changes.json --project-root-dir /path/to/project

# Inspect entries covered by a patch / 枚举 patch 覆盖的文件条目
uni-diff-patch inspect output.diff

# Remove entries by path or entry index / 按路径或条目序号删除 patch 条目
uni-diff-patch remove output.diff --path src/main.py
uni-diff-patch remove output.diff --entry-index 0

# Validate a patch / 验证 patch
uni-diff-patch validate output.diff
uni-diff-patch validate output.diff --git-check --project-root-dir /path/to/repo

# Start MCP server / 启动 MCP 服务器
uni-diff-patch serve
```

## MCP Configuration / MCP 配置

```json
{
  "mcpServers": {
    "uni-diff-patch": {
      "command": "uvx",
      "args": ["uni-diff-patch", "serve"]
    }
  }
}
```

## Entry Lifecycle / 条目生命周期

Patch files are treated as an ordered store of per-file entries. The patch path stored in the `---` / `+++` headers is the identity key. Rename entries occupy both the source and destination paths.

Patch 文件会被当作有序的文件条目集合。`---` / `+++` header 中的 patch path 是身份键；rename 条目同时占用源路径和目标路径。

| Mode / 模式 | Behavior / 行为 |
|-------|---------|
| `append` | Default. Add new entries and fail if any occupied path already exists. / 默认模式。追加新条目；若路径已存在则失败。 |
| `upsert` | Replace matching entries, append absent entries, and remove a matching entry when the recomputed diff is empty. / 替换已有条目、追加不存在的条目；若重新计算后无 diff，则删除已有条目。 |
| `overwrite` | Replace the entire output patch file. / 覆写整个输出 patch 文件。 |
| `create_only` | Create a new output patch file and fail if it already exists. / 仅新建；文件已存在则失败。 |

When using absolute file paths, pass `project_root_dir` and keep it constant for the lifetime of a given output patch file. If a later mutation computes no exact path match but finds an existing entry with the same basename, the result includes a non-blocking `POSSIBLE_PROJECT_ROOT_DIR_DRIFT` warning. The `.diff` file itself intentionally remains metadata-free, so callers are responsible for using one stable project root per output patch.

使用绝对路径时请传入 `project_root_dir`，并在同一个输出 patch 文件的整个生命周期中保持不变。若后续操作没有精确路径匹配，但发现已有条目 basename 相同，结果会包含非阻塞 `POSSIBLE_PROJECT_ROOT_DIR_DRIFT` 警告。`.diff` 文件本身不会写入工具私有元数据，因此调用方需要保证同一输出 patch 使用同一项目根目录。

## MCP Result Fields / MCP 返回字段

`generate_patch_tool` keeps the existing fields `patch`, `files_changed`, `self_valid`, and `skipped_files`, and adds:

- `added_entries` / `replaced_entries` / `removed_entries`: what this call did to the entry store. Replacements carry `replaced_path`; empty-diff removals carry `reason: "empty_diff_upsert"`.
- `entries_before` / `entries_after`: entry count around the write. `added_entries` and `replaced_entries` are indexed by their position **after** the write; `removed_entries` by its position **before**. For `overwrite`, `entries_before` and `removed_entries` are `null` if the replaced file could not be parsed — an `OVERWROTE_UNREADABLE_PATCH` warning explains why, and the overwrite still succeeds so a corrupt patch stays repairable.
- `warnings`: non-blocking warnings such as possible project-root drift.

Note `patch` is **this call's generated diff**, not the current contents of the whole patch file. Use `inspect_patch_entries_tool` for the latter. And `self_valid` only means the diff is structurally well-formed — use `validate_patch_tool` with `git_check: true` to check it actually applies.

`inspect_patch_entries_tool` returns `entries`, `entry_count`, `duplicate_paths`, and `tool_policy_ok`.

`remove_patch_entries_tool` returns `removed_entries`, `remaining_count`, and `warnings`. Removing the last entry leaves an empty patch file in place.

`validate_patch_tool` reports per-entry `change_kind` (a pure rename or chmod has `hunk_count: 0` but is not an empty modification), plus `duplicate_paths` and `tool_policy_ok`, so callers can distinguish parseable patches from patches that violate this tool's single-entry-per-path policy.

## JSON Spec Format / JSON Spec 格式

All modes are specified in a `changes` array. Each change requires `file_path` and `mode`.

所有模式通过 `changes` 数组指定，每个变更需要 `file_path` 和 `mode`。

### file_content — Full new content / 完整新内容

Tool reads original from disk and diffs. / 工具从磁盘读取原文件并生成 diff。

```json
{
  "changes": [{
    "file_path": "src/main.py",
    "mode": "file_content",
    "new_content": "def main():\n    print('hello')\n"
  }]
}
```

### search_replace — Targeted replacements / 精准替换（最省 token）

```json
{
  "changes": [{
    "file_path": "src/main.py",
    "mode": "search_replace",
    "replacements": [
      {"search": "print('hello')", "replace": "print('world')"},
      {"search": "old_func()", "replace": "new_func()", "line_hint": 42}
    ]
  }]
}
```

`line_hint`: approximate line number for disambiguation when `search` matches multiple locations.

`line_hint`：当 `search` 匹配多处时，用大致行号消歧（选最近的匹配）。

### create_file — New file / 新建文件

```json
{
  "changes": [{
    "file_path": "src/new_module.py",
    "mode": "create_file",
    "new_content": "# New module\ndef init():\n    pass\n"
  }]
}
```

### delete_file — Remove file / 删除文件

```json
{
  "changes": [{
    "file_path": "src/deprecated.py",
    "mode": "delete_file"
  }]
}
```

### text_pair — Both old and new text / 直接传入新旧文本（不读磁盘）

```json
{
  "changes": [{
    "file_path": "virtual/path.py",
    "mode": "text_pair",
    "old_text": "x = 1\n",
    "new_text": "x = 2\n"
  }]
}
```

### rename_file — Rename/move / 重命名/移动文件

Optionally with content change. / 可同时修改内容。

```json
{
  "changes": [{
    "file_path": "src/old_name.py",
    "mode": "rename_file",
    "new_path": "src/new_name.py",
    "new_content": "# Optional modified content\n"
  }]
}
```

### chmod — Permission change / 权限变更

Optionally with content change. / 可同时修改内容。

```json
{
  "changes": [{
    "file_path": "scripts/deploy.sh",
    "mode": "chmod",
    "old_mode": "100644",
    "new_mode": "100755"
  }]
}
```

### Multi-file patch / 多文件合并 patch

```json
{
  "changes": [
    {"file_path": "src/a.py", "mode": "file_content", "new_content": "..."},
    {"file_path": "src/b.py", "mode": "search_replace", "replacements": [{"search": "old", "replace": "new"}]},
    {"file_path": "src/c.py", "mode": "create_file", "new_content": "..."}
  ],
  "output_path": "changes.diff",
  "write_mode": "upsert",
  "project_root_dir": "/path/to/project"
}
```

## Optional Fields / 可选字段

| Field / 字段 | Default / 默认值 | Description / 说明 |
|-------|---------|------------|
| `encoding` | `"utf-8"` | File encoding for disk-read modes / 磁盘读取模式的文件编码 |
| `context_lines` | `3` | Context lines in diff / diff 中的上下文行数 |
| `output_path` | — | Top-level, writes or mutates a patch file / 顶层字段，写入或修改 patch 文件 |
| `write_mode` | `"append"` | Top-level, one of append/upsert/overwrite/create_only / 顶层字段，控制写入模式 |
| `project_root_dir` | current directory | Top-level, project root for absolute path relativization / 顶层字段，绝对路径相对化的项目根目录 |

## Development / 开发

```bash
uv sync
uv run pytest tests/ -v
```
