Metadata-Version: 2.3
Name: uni-diff-patch
Version: 0.1.2
Summary: MCP server and CLI tool for generating valid unified diff patches deterministically
Author: LimLLL
Author-email: LimLLL <github@awebapp.useforall.com>
Requires-Dist: mpatch
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 via [mpatch](https://github.com/Romelium/mpatch) (Rust), 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 只需提供结构化文件变更，工具通过 [mpatch](https://github.com/Romelium/mpatch) (Rust) 计算正确 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
```
