pi-agent-edit0.2.14
An agent-built edit tool for Pi: lossless tolerant matching, actionable ambiguity recovery, stale-read guards, and recoverable file writes.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-agent-edit
English | 简体中文
一个由 agent 自己开发、为 agent 使用而设计的 edit 工具。 在人类维护者的指导和审查下,AI agent 参与实现、测试与迭代。参与开发的 AI 模型(按维护者提供的名称列出):claude-opus-5.5、gemini-4-Argon、gpt-6.1-sol。这些名称是开发贡献说明,不表示模型提供商的官方背书。
为 agent 设计的 Pi edit 工具替代品。注册同名工具 edit 覆盖内置版本,并包装官方 read 以捕获真实读取版本(Pi 加载时会提示 shadow),参数完全向后兼容 { path, edits: [{ oldText, newText }] }。
解决内置 edit 的哪些问题
| 内置 edit 的不足 | 本扩展的做法 |
|---|---|
| 一旦某条 edit 走了模糊匹配,被触及行上的全角中文标点、弯引号会被静默改成 ASCII(earendil-works/pi#10655) | 归一化只用于定位,替换时原文件中匹配区之外的字符一个都不改 |
| 模糊匹配不处理缩进差异、连续空白长度差异(#7836) | 四级匹配:exact → 忽略行尾空白 → 忽略缩进(按 oldText 与文件逐行的缩进对应关系重建 newText 的缩进,统一使用文件自己的 tab/空格风格,不会产生混排)→ 宽松(空白折叠 + NFKC/引号/破折号/全角归一) |
模型从报错或 read 结果复制时常误带 12│ 行号前缀 |
当常规匹配失败时自动识别并剥离 oldText/newText 的行号前缀(包括本工具 diff 里的 + 12│ 行;newText 中混有手写新行时只剥离带前缀的行) |
| 匹配失败只回报"找不到",模型通常要重新 read 整个文件 | 返回最相似的候选区域(带行号和相似度,惰性缓存 + 提前剪枝加速)、逐行"file vs yours"差异;若 newText 在文件中唯一存在且不是 oldText 的一部分,提示"可能已应用"(同时仍给出候选区,避免误报掩盖真实问题) |
| oldText 出现多次只能报错,模型只能重新 read | 报错里给出最多 5 处出现的带行号上下文,并在最多扩展 6 行、400 字符内尝试给出“唯一 oldText”(从文件原样截取,可直接复制使用);报错中最多列出 12 个匹配行号,可以立即作为 nearLine 重试(同一行多个匹配无法靠行号区分)——即使已读视图已过期,只要文件内容没变就接受;也支持 replaceAll 全部替换 |
| 只能按文本替换 | 新增 startLine/endLine 区间替换/删除、afterLine 插入(0 = 文件开头,精确处理空文件) |
| — | 行号操作有陈旧保护:read 记录工具实际读到的字节版本,write 记录输入内容的版本;每个改动块都没改变行数时保留原有行号视图。发生行数变化后,仍可直接使用最新成功 edit 报告中实际展示的新文件行号,省略区域和跨越未展示区域的范围不授权。nearLine 在多处匹配中选择时也受保护。会话切换、分叉、树跳转和压缩会清空视图 |
| 结果反馈不利于继续编辑 | 返回紧凑的带新行号的 diff(含上下文),模型一般不需要再 read |
| — | dryRun 预览;JSON 文件被改坏时给出警告 |
| — | 原子写入(临时文件 + fsync + rename,多硬链接文件自动原地写保护 inode),保留 BOM、CRLF、文件权限、属主/属组(无法恢复时改为原地写)、符号链接;只有目录不可写这类错误才回退原地写,磁盘满等错误直接失败、原文件不动;多条 edit 全有或全无(自动幂等去重完全相同的重复项) |
| — | 并发修改检测:rename 前(原地写则在同一文件描述符上写入前)再次核对文件内容是否仍是本次编辑读取时的版本,被其他进程改过就拒绝写入,并要求重新读取 |
| — | 原地写可恢复:不用 O_TRUNC,先覆盖再截断,原始内容保留在内存并备份到系统临时目录;写到一半失败会自动写回原文;连回滚也失败时,报错给出备份文件路径 |
| 容错匹配会把未改动行的空白、引号一起改掉 | 非精确匹配时,newText 中与 oldText 完全相同的行视为未改动,原样保留文件中的那一行(字符串里的空白、Makefile 的 tab、对齐空格、弯引号都不动);只有真正改动的行才会重缩进 |
| — | Python、YAML、Makefile 等空白敏感文件:容错匹配、tab/空格混用、YAML 中的 tab 缩进、Makefile 中用空格开头的 recipe 行会在结果开头给出 WARNING(文本检查,不是语法分析) |
| — | 非 UTF-8 文件(GBK、Latin-1 等)直接拒绝编辑,而不是把全文的非 ASCII 字节替换成乱码;混合换行文件中被改动的 CRLF 行保持 CRLF |
部分模型会发 old_str/new_str/start_line、字符串形式的 edits |
prepareArguments 兼容所有参数的 camelCase 与 snake_case 别名;行号参数接受 "12" 这样的整数字符串,null 视为未设置 |
参数
{
"path": "src/app.ts",
"dryRun": false, // 可选
"edits": [
// 每项只能用一种定位方式:
{ "oldText": "foo(1)", "newText": "foo(2)" }, // 文本替换
{ "oldText": "x = 1", "newText": "x = 2", "nearLine": 120 }, // 多处出现时取最接近 120 行的
{ "oldText": "oldName", "newText": "newName", "replaceAll": true }, // 全部替换
{ "startLine": 10, "endLine": 12, "newText": "..." }, // 替换 10–12 行(newText 为空则删除)
{ "afterLine": 0, "newText": "import x from 'x';" } // 在第 0 行后(文件开头)插入
]
}
所有 edit 都基于调用前的原始文件定位,不能重叠。endLine 只能和 startLine 搭配,replaceAll/nearLine 只能用于 oldText,混用会报错而不是被静默忽略。同一调用里如果还有别的真实修改,oldText 与 newText 完全相同的条目会被跳过并在结果里注明;如果每一条都是空改,整次调用失败、不写文件。追加到文件末尾时 afterLine 等于当前行数,不是行数加一。path 支持模型常写的 @src/app.ts 形式(同名的字面 @ 文件优先)。
新建文件与连续行号编辑
- 通过成功的
write创建文件后,不用额外read就能按行编辑。通过 shell 创建的文件没有工具视图,需先read;上下文压缩或切换后也一样。如果宿主的write + then_run已写入文件,但后续命令失败导致整体工具报错,也不会建立视图,需先read,不能仅凭文件存在推断写入已成功完成。 read的offset/limit可建立当前版本视图:hash 来自底层实际读取的 Buffer,不是截取后的显示文本,也不事后重读磁盘。这是版本校验,不代表每一行都已显示,仍需确认目标坐标。- 编辑增删行后,只认最新成功报告实际打印的新行号:删除行的旧行号、省略的改动块、截断的中间区域都不能作为授权;
dryRun不建立新视图。 - 外部内容变化立即使上述坐标失效,即使文件大小和修改时间都被保留。此时重新
read,或改用oldText。
安装
已发布到 npm,并收录于 Pi 包画廊:
pi install npm:pi-agent-edit
# 或仅在当前项目安装
pi install -l npm:pi-agent-edit
# 或仅试用一次
pi -e npm:pi-agent-edit
本地源码也可使用 pi install ./pi-agent-edit 安装。然后重新启动 Pi 或执行 /reload。
本扩展有意覆盖 edit 并包装官方 read 工具;加载时的 shadow 提示是预期行为。不要同时加载另一套 edit 或 read 替代工具。只保留一个安装来源,避免同时从本地扩展目录和 npm 重复加载。卸载使用 pi remove npm:pi-agent-edit,然后重启或 /reload。
请使用当前版本的 Pi(@earendil-works/pi-coding-agent)。包测试对着 1.3.0 的 peer 运行;已加载的工具在 Pi 1.5.2 上实际用过。替换内置 edit 的能力以及具体参数格式取决于宿主版本,不保证兼容所有旧版 Pi 或第三方分支。
测试
engine.ts 和 writer.ts 不依赖 Pi,可独立测试:
npm i -D tsx
npx tsx --test test/engine.test.ts test/regression.engine.test.ts test/hardening.engine.test.ts test/writer.test.ts
test/*extension.test.ts 需要安装 Pi(peer 依赖),且 Pi 要求 Node >= 22.19.0(它用了 node:fs 的 globSync),Node 20 上这些用例连模块都加载不了。装好后 npm test 跑全部测试,npm run typecheck 做类型检查(已内置本地 typescript devDependency,不依赖全局 tsc);两者在 CI(.github/workflows/pi-agent-edit.yml,Node 22/24)中对 pi-agent-edit/** 的改动自动运行。regression.*.test.ts 是 0.2.1 修复的回归用例,hardening.*.test.ts 和 writer.test.ts 是 0.2.2/0.2.3 的用例(writer.test.ts 用故障注入覆盖 rename/fsync 失败、原地写半途失败与回滚失败、并发修改)。属主相关的几条需要 root 权限,否则自动跳过。
许可证
MIT。本扩展在本地运行,不需要独立的 API key 或模型服务;AI 模型参与开发,而不是在每次编辑时由扩展调用。
仍然存在的限制
- 检查与 rename 之间仍有极短的竞态窗口。 POSIX 没有“比较后再 rename”的原子操作;本扩展把检查放在 rename 之前的最后一步,窗口是微秒级,但不为零。它也不是文件锁:不会阻止别的编辑器之后覆盖本次修改。
- 原地写不是原子的,只是可恢复的。 进程在写入中途被强杀(kill -9、断电)时没有机会回滚,文件可能是新旧内容的混合;备份文件只在本次调用期间存在。正常的 I/O 错误会被回滚。
- 网络盘和特殊挂载无法由本地测试证明。 NFS/SMB 上 rename、fsync 的语义取决于服务端和挂载参数;对目录 fsync 不被支持时会静默跳过。
- 空白检查是文本启发式,不是语法分析。 关键修改仍应看 diff、跑编译或测试。