pi-agent-edit 0.0.0-stage → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,100 @@
1
+ # Changelog
2
+
3
+ ## 0.2.3
4
+
5
+ ### oldText 多处匹配时不必重新读取
6
+
7
+ 0.2.2 及之前只报 `matches 3 places (lines 2, 6, 10)`,模型看不到这几处各自的内容,通常只能重新 read。现在报错包含:
8
+
9
+ - **每处出现的上下文**:前后各一行,带 ` 12│` 行号(与本工具其他输出格式一致,误带进 oldText 时会被自动剥离);超长的多行匹配中间折叠。最多展示 5 处上下文,匹配行号最多列出 12 个。
10
+ - **每处的“唯一 oldText”**:从文件原样截取,逐步向上、向下扩展整行(最多 6 行、400 字符),直到在全文中恰好出现一次;以 JSON 字符串给出,可直接复制。提示模型把扩展进来的上下文行原样放进 newText。实在无法唯一时改为提示用 `nearLine`;同一行多个匹配会说明行号无法区分,需在唯一上下文中只替换目标出现位置。某个扩展窗口超长时仍尝试其他方向的窗口。
11
+ - **报错中的行号可以立即作为 `nearLine` 使用**:这些行号是对当前文件内容算出来的,所以即使已读视图过期(从未 read、外部修改后、行数变化后),只要文件哈希与报错时一致、且 `nearLine` 恰好等于报错列出的某个匹配行,就接受;文件一旦变化即失效,猜测的近似行号仍会被拒绝。会话切换等事件同样会清空。
12
+
13
+ 用例在 `test/hardening.engine.test.ts`、`test/hardening.extension.test.ts`。
14
+
15
+ ### 行为变化
16
+
17
+ - `EditError` 新增可选字段 `nearLineOffers`,`ApplyOptions` 新增 `offeredNearLines`。
18
+ - 多处匹配的报错文本变长、格式变化;按旧文本做精确匹配的调用方需要调整(`matches N places (lines …)` 这一句保持不变)。
19
+
20
+ ## 0.2.2
21
+
22
+ 针对 0.2.1 审查后保留的四项风险。新用例在 `test/writer.test.ts`、`test/hardening.engine.test.ts`、`test/hardening.extension.test.ts`。
23
+
24
+ ### 写入(新模块 `writer.ts`,不依赖 Pi)
25
+
26
+ - **并发修改不再被静默覆盖**:`atomicWrite(target, content, { expectedHash })` 在临时文件写好并 fsync 之后、rename 之前,重新读取目标并核对哈希;原地写路径则在同一个文件描述符上读出、核对、写入。不一致时抛出 `ConcurrentModificationError`,什么都不写,`edit` 同时清除该文件的已读视图,迫使模型重新读取。0.2.1 中,读取后到写入前被其他进程修改的内容会被本次编辑覆盖。
27
+ - **原地写改为可恢复**:0.2.1 用 `fs.writeFile`(O_TRUNC)原地写,写到一半失败会留下被截断或半写的文件。现在:不截断地覆盖写 → `ftruncate` 到新长度 → fsync;原始字节保留在内存,并以 0600 权限备份到系统临时目录。任何一步失败都会写回原文;回滚也失败时,错误信息给出备份路径。成功后删除备份。
28
+ - **临时文件在 rename 前 fsync,rename 后对目录做尽力 fsync**;临时文件 fsync 失败按存储错误处理,原文件不动。
29
+ - 硬链接、无法 chown、rename 被拒(EXDEV/EBUSY 等)这几条原地写路径全部改用上述可恢复写法。
30
+
31
+ ### 容错匹配
32
+
33
+ - **非精确匹配时,未改动的行原样保留**:`newText` 中与 `oldText` 某行完全相同的行(按 LCS 对齐,插入或移动其他行不影响配对),写回文件中原来那一行,而不是模型输入的版本。0.2.1 中,宽松匹配会把未改动行里的连续空白(包括字符串字面量内部)、弯引号、全角标点改成模型输入的形式;trailing-ws 匹配会去掉未改动行的行尾空白;Makefile 中未改动的 recipe 行也会经过重缩进。混合换行文件中被保留的行同时保留自己的 CRLF。
34
+ - **空白敏感文件给出警告**(`whitespaceWarnings`,结果开头的 `WARNING:` 行,dryRun 也会显示):Python/YAML/Makefile 中走了 indent/loose 匹配;Python 新增行 tab/空格混用或与文件风格不一致;YAML 新增行用 tab 缩进;Makefile 中紧跟规则头或 recipe 行、却以空格开头的新增行。只检查本次新增的行,不报告文件里原有的问题。
35
+
36
+ ### 行为变化(可能影响调用方)
37
+
38
+ - `atomicWrite` 移到 `writer.ts`,`index.ts` 仍然重新导出;新增可选第三参数 `{ expectedHash }`,不传时行为与之前一致(但原地写已改为可恢复写法)。
39
+ - `writer.ts` 导出 `sha`、`writeInPlace`、`ConcurrentModificationError`;`engine.ts` 导出 `whitespaceWarnings`。
40
+ - 非精确匹配下,未改动行的空白不再被规范化(见上),依赖旧行为“顺手清理空白”的调用方会看到差异。
41
+
42
+ ### 验收补充
43
+
44
+ - 大块容错编辑超过 LCS 预算时,先剥离相同首尾行;剩余对齐仍超过预算则拒写,要求拆分编辑或提供精确原文,不再按位置回退而静默改变字符串空白。
45
+ - 原地写在备份完成后再次核对同一文件描述符的内容,拒绝覆盖备份 I/O 期间发生的修改。
46
+ - lockfile 版本同步至 0.2.2;旧 ToolCall 测试补齐类型字段,完整类型检查通过。
47
+
48
+ ### 已知限制
49
+
50
+ - 核对与 rename 之间仍有竞态窗口(POSIX 没有比较后 rename 的原子操作);这不是文件锁,也不阻止别的程序之后覆盖。
51
+ - 原地写可恢复但不是原子的:进程被强杀或断电时来不及回滚;最终复核之后及写入/回滚期间仍可能与其他写入者竞争,不提供文件锁。
52
+ - NFS/SMB 等网络文件系统上的 rename/fsync 语义无法由本地测试证明。
53
+ - 空白检查是文本启发式,不理解语法;关键修改仍需看 diff、跑编译或测试。
54
+ - 原子写入产生新 inode,ACL 和 xattr 不会被复制(同 0.2.1)。
55
+
56
+ ## 0.2.1
57
+
58
+ 每条修复在 `test/regression.engine.test.ts` 或 `test/regression.extension.test.ts` 里都有对应用例,注释写明了 0.2.0 的错误行为。
59
+
60
+ ### 会静默写错或损坏文件的问题
61
+
62
+ - **非 UTF-8 文件被整体损坏**:0.2.0 以宽松 UTF-8 读取,GBK、Latin-1 等文件的每个非 ASCII 字节都会变成 U+FFFD 并写回,连未编辑的行也不例外。现在用严格解码,失败时拒绝编辑并提示先转码。二进制检测改为在原始字节上进行。
63
+ - **行号陈旧保护只比较总行数**:一次编辑在顶部 +1 行、底部 −1 行时总行数不变,保护被绕过,后续 `startLine` 改错行。现在要求每个改动块的行数都不变(`ApplyResult.linesShifted`)。
64
+ - **"已应用,勿重复"误报**:删除型编辑(newText 是 oldText 的一部分)或 newText 很常见(如 `}`)时,会误报"已应用"并且不再给出候选区。现在只在 newText 唯一存在、且不包含于 oldText 时才提示,并且无论如何都会给出最接近的候选区。
65
+ - **newText 部分行带行号前缀时,前缀被写进文件**:oldText 带前缀而 newText 混有手写新行时,0.2.0 会把 ` 12│` 原样写入。现在逐行剥离带前缀的行。
66
+
67
+ ### 中等
68
+
69
+ - **重缩进产生 tab/空格混排**:0.2.0 只替换参考行的前缀,其余空白原样保留,例如 ` \treturn`,在 Python 中会触发 TabError。现在根据 oldText 与文件逐行的缩进对应关系计算映射,整段重建前导空白并统一使用文件自己的风格。当 oldText 体现出缩进比例时(例如 2 空格对 4 空格),newText 中更深的层级会按同样比例换算。
70
+ - **`nearLine` 不受陈旧保护**:行号偏移后,`nearLine` 会静默选中另一处匹配。现在行号不可信时,若需要用 nearLine 在多处匹配中做选择,会拒绝并说明原因;唯一匹配不受影响。
71
+ - **原子写入的兜底逻辑可能丢数据**:0.2.0 遇到任何临时文件错误都会退回到会截断原文件的原地写,磁盘满时原文件会丢失。现在只对 EACCES、EPERM、EROFS、EXDEV、EBUSY、ENOTSUP、EINVAL、ENAMETOOLONG 这类"无法在此创建或替换文件"的错误回退,其他错误直接失败,原文件保持不变。
72
+ - **rename 后属主/属组丢失**:现在会把临时文件 chown 回原属主;没有权限时改为原地写,保留原 inode。
73
+ - **CRLF 文件中未触及的 `\r\r\n` 被改成 `\r\n`**:`encode` 改为只把 LF 映射回 CRLF。
74
+
75
+ ### 较轻 / 体验
76
+
77
+ - `@src/app.ts` 形式的路径现在可以解析(字面以 `@` 开头的文件优先)。read 时记录的已读视图使用同一套解析。
78
+ - 从本工具成功 diff 中复制的 `+ 12│...` 行现在能被识别为行号前缀。
79
+ - 行匹配路径下 newText 末尾恰好去掉一个换行:`X\n` 不产生空行,`X\n\n` 产生一个空行(0.2.0 下是 0 个或 2 个)。
80
+ - `prepareArguments` 接受 `"12"` 这样的整数字符串;`null` 视为未设置(0.2.0 下会报错或残留到 schema 校验)。
81
+ - 混合换行文件中,被行级操作触及的 CRLF 行保持 CRLF;插入行跟随相邻行的换行风格。
82
+ - `oldText` 与 `endLine`、行号定位与 `replaceAll`/`nearLine` 混用时,现在会报错,而不是被静默忽略。
83
+ - 除 `session_start` 外,还会在 `session_switch`、`session_fork`、`session_branch`、`session_tree`、`session_compact` 时清空已读视图。不同 Pi 版本的事件名可能不同,注册不存在的事件名不会产生影响,但建议对照所用版本确认。
84
+
85
+ ### 行为变化(可能影响调用方)
86
+
87
+ - `applyEdits(decoded, edits, opts?)` 新增可选参数 `{ lineNumbersTrusted, untrustedReason }`,`ApplyResult` 新增 `linesShifted`。
88
+ - `index.ts` 新增命名导出 `atomicWrite`。
89
+ - `test/extension.test.ts` 中原先断言 `"1"` 会被拒绝的用例,已改为断言 `" 1.0"` 会被拒绝,因为整数字符串现在会被接受。
90
+
91
+ ### 部署验收补充修复
92
+
93
+ - 行号可信度按原始编辑区间的换行差计算(含 EOF 无末尾换行边界),不再依赖合并后的展示 hunk,避免相邻的 +1/−1 编辑抵消后绕过保护。
94
+ - chown 只有 EPERM/EACCES 才允许原地写回退;EIO/ENOSPC/EDQUOT 直接拒绝并保留原文件。恢复顺序改为先 chown 再 chmod,保留 setuid/setgid 位。
95
+ - 仅匹配到一个缩进层级时,使用文件的缩进步长推断新层级,不再把绝对嵌套深度当作缩进单位比例。
96
+
97
+ ### 已知限制
98
+
99
+ - 原子写入会产生新 inode,ACL 和扩展属性(xattr)不会被复制。属主不同且无法 chown 时会走原地写,从而保留这些属性。
100
+ - 原地写本身不是原子的。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bjstan1979
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,126 @@
1
- # Temporary Holding Version
1
+ # pi-agent-edit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **An edit tool built by an agent, for agents.**
4
+
5
+ A replacement for Pi's built-in `edit` tool, developed and iterated by AI agents under human direction and review. Instead of making an agent re-read a file after every mismatch, it provides actionable recovery hints while keeping untouched file content intact.
6
+
7
+ [简体中文](./README.zh-CN.md) · [Changelog](./CHANGELOG.md) · [MIT license](./LICENSE)
8
+
9
+ ## Built by agents
10
+
11
+ AI models participating in development, using the names supplied by the maintainer:
12
+
13
+ - **claude-opus-5.5**
14
+ - **gemini-4-Argon**
15
+ - **gpt-6.1-sol**
16
+
17
+ Agents helped implement, test, review, and refine the tool they themselves use to edit code. These are development credits, not endorsements by the model providers. Human review and tests still matter: agent-built does not mean infallible.
18
+
19
+ The extension runs locally. It does **not** call these models or require its own API key or model subscription.
20
+
21
+ ## Install
22
+
23
+ Once published to npm:
24
+
25
+ ```bash
26
+ pi install npm:pi-agent-edit
27
+ ```
28
+
29
+ Or install for the current project only:
30
+
31
+ ```bash
32
+ pi install -l npm:pi-agent-edit
33
+ ```
34
+
35
+ Try it for one invocation without saving an installation:
36
+
37
+ ```bash
38
+ pi -e npm:pi-agent-edit
39
+ ```
40
+
41
+ Before publication, use a local source checkout instead: `pi install ./pi-agent-edit`.
42
+
43
+ Restart Pi or run `/reload` after installing. This package intentionally registers a tool named **`edit`**, shadowing the built-in tool; a shadowing notice is expected. Avoid loading another replacement for the same tool, and remove any manually copied version before installing from npm.
44
+
45
+ To uninstall, run `pi remove npm:pi-agent-edit`, then restart or `/reload` to restore the built-in tool (provided no other override remains).
46
+
47
+ **Compatibility:** tested with Pi 1.3.0 (`@earendil-works/pi-coding-agent`). Use a current Pi release. Tool overriding, argument formats, and host APIs can differ in older releases or third-party forks; compatibility with every Pi version is not guaranteed. Pi supplies the host peer dependencies; there is no separate build step.
48
+
49
+ ## Why another edit tool?
50
+
51
+ Agents rarely get every copied string exactly right. They encounter indentation differences, repeated blocks, copied line-number prefixes, and files changed by another process. This tool makes those cases recoverable without silently normalizing unrelated content.
52
+
53
+ - **Lossless tolerant matching.** Tries exact text, trailing-whitespace tolerance, indentation tolerance, then relaxed whitespace/Unicode matching. Normalization locates the match; it does not rewrite the whole file. Unchanged lines in tolerant replacements retain their original content.
54
+ - **Actionable ambiguity errors.** Shows context for up to five occurrences, attempts unique whole-line `oldText` suggestions within six extra lines and 400 characters, and lists up to 12 matching line numbers. Retry with a listed `nearLine` without re-reading while the file hash stays unchanged. Same-line duplicates require distinguishing context, not a line-number tie-break.
55
+ - **Useful mismatch feedback.** Suggests similar numbered regions and line-by-line differences. Detects some likely already-applied edits without treating them as success.
56
+ - **Agent-friendly input.** Handles copied line-number prefixes, camelCase/snake_case argument aliases, integer strings for line numbers, and smart relative re-indentation.
57
+ - **More than search and replace.** Supports line-range replacement/deletion, insertion, `replaceAll`, and `dryRun`.
58
+ - **Stale-read guards.** Line-number edits require a sufficiently fresh read/write view. Session changes and compaction clear that view; hash-bound ambiguity offers are separately invalidated by content changes.
59
+ - **Safer writes.** All edits resolve against the original file and are applied all-or-nothing. Uses temporary-file/fsync/rename writes where possible, detects concurrent content changes, and uses recoverable in-place writes when necessary. Preserves BOM, CRLF, permissions, ownership where possible, symlinks, and hard-link identity via in-place writing.
60
+ - **Readable results.** Compact numbered diffs support the next edit without another read. JSON and whitespace-sensitive file checks provide warnings, not a substitute for parsing or tests.
61
+ - **Encoding guard.** Rejects non-UTF-8 files rather than corrupting their non-ASCII bytes.
62
+
63
+ ## Usage
64
+
65
+ The agent calls `edit`; you do not need to run a separate command for each change. The original text-replacement format remains available:
66
+
67
+ ```json
68
+ {
69
+ "path": "src/app.ts",
70
+ "edits": [
71
+ { "oldText": "foo(1)", "newText": "foo(2)" }
72
+ ]
73
+ }
74
+ ```
75
+
76
+ Additional locators and options:
77
+
78
+ ```jsonc
79
+ {
80
+ "path": "src/app.ts",
81
+ "dryRun": true,
82
+ "edits": [
83
+ { "oldText": "x = 1", "newText": "x = 2", "nearLine": 120 },
84
+ { "oldText": "oldName", "newText": "newName", "replaceAll": true },
85
+ { "startLine": 10, "endLine": 12, "newText": "replacement" },
86
+ { "afterLine": 0, "newText": "import x from 'x';" }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ These illustrate independent edit forms: an actual batch must not contain overlapping edits. Every edit targets the file **before** the call, not the output of earlier edits in the same batch. Use exactly one locator per edit. `endLine` requires `startLine`; `nearLine` and `replaceAll` only apply to `oldText`. Empty `newText` deletes a matched region or line range. `afterLine: 0` inserts at the beginning; line insertion uses line-oriented newline semantics.
92
+
93
+ ### Recovering from repeated text
94
+
95
+ If `oldText` matches several places, the error offers numbered snippets and, when available, a unique larger `oldText`. Either:
96
+
97
+ 1. Copy the unique suggestion and preserve the extra context unchanged in `newText`.
98
+ 2. Keep `oldText` and use a listed matching line as `nearLine` while the file remains unchanged.
99
+ 3. Set `replaceAll: true` if every occurrence should change.
100
+
101
+ `nearLine` cannot distinguish matches that start on the same line. If the file changes after an error, its hash-bound line offers no longer authorize a stale-line retry.
102
+
103
+ ## Safety and limitations
104
+
105
+ - Tolerant matching is not a semantic code transformation. Review the diff and run your compiler/tests, especially for Python, YAML, and Makefiles.
106
+ - Concurrent-change detection is best-effort, not a file lock. POSIX does not provide atomic compare-and-rename; a race remains between the final check and rename.
107
+ - In-place fallback is recoverable on ordinary I/O failure, **not atomic**. A forced termination or power loss can interrupt it before rollback. Recovery backups normally exist only during the call; rollback failure reports the retained backup path.
108
+ - Network filesystems and special mounts can have different rename/fsync guarantees. Unsupported directory fsync is skipped.
109
+ - Non-UTF-8 files are deliberately unsupported. JSON/whitespace diagnostics are warnings and heuristics, not full syntax validation.
110
+
111
+ ## Development
112
+
113
+ From a source checkout with Pi's peer dependencies available:
114
+
115
+ ```bash
116
+ npm install
117
+ npm test
118
+ npx tsc --noEmit
119
+ npm pack --dry-run
120
+ ```
121
+
122
+ The pure editing engine and writer can be tested independently of Pi. Extension integration tests need the host peer dependencies. Ownership-sensitive tests skip when not run as root. Tests and development configuration are intentionally excluded from the npm tarball.
123
+
124
+ ## License
125
+
126
+ [MIT](./LICENSE). Maintained by **bjstan1979**.
@@ -0,0 +1,87 @@
1
+ # pi-agent-edit
2
+
3
+ [English](./README.md) | 简体中文
4
+
5
+ **一个由 agent 自己开发、为 agent 使用而设计的 edit 工具。** 在人类维护者的指导和审查下,AI agent 参与实现、测试与迭代。参与开发的 AI 模型(按维护者提供的名称列出):**claude-opus-5.5**、**gemini-4-Argon**、**gpt-6.1-sol**。这些名称是开发贡献说明,不表示模型提供商的官方背书。
6
+
7
+ 为 agent 设计的 Pi `edit` 工具替代品。注册同名工具 `edit` 覆盖内置版本(Pi 加载时会提示 shadow),参数完全向后兼容 `{ path, edits: [{ oldText, newText }] }`。
8
+
9
+ ## 解决内置 edit 的哪些问题
10
+
11
+ | 内置 edit 的不足 | 本扩展的做法 |
12
+ |---|---|
13
+ | 一旦某条 edit 走了模糊匹配,被触及行上的全角中文标点、弯引号会被静默改成 ASCII(earendil-works/pi#10655) | 归一化**只用于定位**,替换时原文件中匹配区之外的字符一个都不改 |
14
+ | 模糊匹配不处理缩进差异、连续空白长度差异(#7836) | 四级匹配:exact → 忽略行尾空白 → 忽略缩进(按 oldText 与文件逐行的缩进对应关系重建 newText 的缩进,统一使用文件自己的 tab/空格风格,不会产生混排)→ 宽松(空白折叠 + NFKC/引号/破折号/全角归一) |
15
+ | 模型从报错或 read 结果复制时常误带 ` 12│` 行号前缀 | 当常规匹配失败时自动识别并剥离 `oldText`/`newText` 的行号前缀(包括本工具 diff 里的 `+ 12│` 行;newText 中混有手写新行时只剥离带前缀的行) |
16
+ | 匹配失败只回报"找不到",模型通常要重新 read 整个文件 | 返回最相似的候选区域(带行号和相似度,惰性缓存 + 提前剪枝加速)、逐行"file vs yours"差异;若 newText 在文件中唯一存在且不是 oldText 的一部分,提示"可能已应用"(同时仍给出候选区,避免误报掩盖真实问题) |
17
+ | oldText 出现多次只能报错,模型只能重新 read | 报错里给出最多 5 处出现的带行号上下文,并在最多扩展 6 行、400 字符内尝试给出“唯一 oldText”(从文件原样截取,可直接复制使用);报错中最多列出 12 个匹配行号,可以立即作为 `nearLine` 重试(同一行多个匹配无法靠行号区分)——即使已读视图已过期,只要文件内容没变就接受;也支持 `replaceAll` 全部替换 |
18
+ | 只能按文本替换 | 新增 `startLine/endLine` 区间替换/删除、`afterLine` 插入(0 = 文件开头,精确处理空文件) |
19
+ | — | 行号操作有**陈旧保护**:要求模型在文件最近一次外部变更后用 `read` 或 `write` 访问过它;只有上一次 `edit` 的**每个改动块**都没有改变行数时,才允许直接继续使用行号编辑(只看总行数会漏掉"顶部 +1、底部 −1"这类整体偏移)。`nearLine` 需要用来在多处匹配中选择时也受同样保护。会话切换、分叉、树跳转和压缩会清空已读记录 |
20
+ | 结果反馈不利于继续编辑 | 返回紧凑的带新行号的 diff(含上下文),模型一般不需要再 read |
21
+ | — | `dryRun` 预览;JSON 文件被改坏时给出警告 |
22
+ | — | 原子写入(临时文件 + fsync + rename,多硬链接文件自动原地写保护 inode),保留 BOM、CRLF、文件权限、属主/属组(无法恢复时改为原地写)、符号链接;只有目录不可写这类错误才回退原地写,磁盘满等错误直接失败、原文件不动;多条 edit 全有或全无(自动幂等去重完全相同的重复项) |
23
+ | — | **并发修改检测**:rename 前(原地写则在同一文件描述符上写入前)再次核对文件内容是否仍是本次编辑读取时的版本,被其他进程改过就拒绝写入,并要求重新读取 |
24
+ | — | **原地写可恢复**:不用 O_TRUNC,先覆盖再截断,原始内容保留在内存并备份到系统临时目录;写到一半失败会自动写回原文;连回滚也失败时,报错给出备份文件路径 |
25
+ | 容错匹配会把未改动行的空白、引号一起改掉 | 非精确匹配时,`newText` 中与 `oldText` 完全相同的行视为未改动,**原样保留文件中的那一行**(字符串里的空白、Makefile 的 tab、对齐空格、弯引号都不动);只有真正改动的行才会重缩进 |
26
+ | — | Python、YAML、Makefile 等空白敏感文件:容错匹配、tab/空格混用、YAML 中的 tab 缩进、Makefile 中用空格开头的 recipe 行会在结果开头给出 `WARNING`(文本检查,不是语法分析) |
27
+ | — | 非 UTF-8 文件(GBK、Latin-1 等)直接拒绝编辑,而不是把全文的非 ASCII 字节替换成乱码;混合换行文件中被改动的 CRLF 行保持 CRLF |
28
+ | 部分模型会发 `old_str/new_str/start_line`、字符串形式的 `edits` | `prepareArguments` 兼容所有参数的 camelCase 与 snake_case 别名;行号参数接受 `"12"` 这样的整数字符串,`null` 视为未设置 |
29
+
30
+ ## 参数
31
+
32
+ ```jsonc
33
+ {
34
+ "path": "src/app.ts",
35
+ "dryRun": false, // 可选
36
+ "edits": [
37
+ // 每项只能用一种定位方式:
38
+ { "oldText": "foo(1)", "newText": "foo(2)" }, // 文本替换
39
+ { "oldText": "x = 1", "newText": "x = 2", "nearLine": 120 }, // 多处出现时取最接近 120 行的
40
+ { "oldText": "oldName", "newText": "newName", "replaceAll": true }, // 全部替换
41
+ { "startLine": 10, "endLine": 12, "newText": "..." }, // 替换 10–12 行(newText 为空则删除)
42
+ { "afterLine": 0, "newText": "import x from 'x';" } // 在第 0 行后(文件开头)插入
43
+ ]
44
+ }
45
+ ```
46
+
47
+ 所有 edit 都基于**调用前的原始文件**定位,不能重叠。`endLine` 只能和 `startLine` 搭配,`replaceAll`/`nearLine` 只能用于 `oldText`,混用会报错而不是被静默忽略。`path` 支持模型常写的 `@src/app.ts` 形式(同名的字面 `@` 文件优先)。
48
+
49
+ ## 安装
50
+
51
+ 发布到 npm 后,可通过 Pi 安装(发布前请使用本地路径):
52
+
53
+ ```bash
54
+ pi install npm:pi-agent-edit
55
+ # 或仅在当前项目安装
56
+ pi install -l npm:pi-agent-edit
57
+ # 或仅试用一次
58
+ pi -e npm:pi-agent-edit
59
+ ```
60
+
61
+ 本地源码也可使用 `pi install ./pi-agent-edit` 安装。然后重新启动 Pi 或执行 `/reload`。
62
+
63
+ 本扩展有意注册同名 `edit` 工具,覆盖内置版本;加载时的 shadow 提示是预期行为。只保留一个安装来源,避免同时从本地扩展目录和 npm 重复加载。卸载使用 `pi remove npm:pi-agent-edit`,然后重启或 `/reload`。
64
+
65
+ 请使用当前版本的 Pi(`@earendil-works/pi-coding-agent`);本版本在 Pi 1.3.0 上验证。替换内置 edit 的能力以及具体参数格式取决于宿主版本,不保证兼容所有旧版 Pi 或第三方分支。
66
+
67
+ ## 测试
68
+
69
+ `engine.ts` 和 `writer.ts` 不依赖 Pi,可独立测试:
70
+
71
+ ```bash
72
+ npm i -D tsx
73
+ npx tsx --test test/engine.test.ts test/regression.engine.test.ts test/hardening.engine.test.ts test/writer.test.ts
74
+ ```
75
+
76
+ `test/*extension.test.ts` 需要安装 Pi(peer 依赖)。装好后 `npm test` 跑全部测试。`regression.*.test.ts` 是 0.2.1 修复的回归用例,`hardening.*.test.ts` 和 `writer.test.ts` 是 0.2.2/0.2.3 的用例(`writer.test.ts` 用故障注入覆盖 rename/fsync 失败、原地写半途失败与回滚失败、并发修改)。属主相关的几条需要 root 权限,否则自动跳过。
77
+
78
+ ## 许可证
79
+
80
+ [MIT](./LICENSE)。本扩展在本地运行,不需要独立的 API key 或模型服务;AI 模型参与开发,而不是在每次编辑时由扩展调用。
81
+
82
+ ## 仍然存在的限制
83
+
84
+ - **检查与 rename 之间仍有极短的竞态窗口。** POSIX 没有“比较后再 rename”的原子操作;本扩展把检查放在 rename 之前的最后一步,窗口是微秒级,但不为零。它也不是文件锁:不会阻止别的编辑器之后覆盖本次修改。
85
+ - **原地写不是原子的,只是可恢复的。** 进程在写入中途被强杀(kill -9、断电)时没有机会回滚,文件可能是新旧内容的混合;备份文件只在本次调用期间存在。正常的 I/O 错误会被回滚。
86
+ - **网络盘和特殊挂载无法由本地测试证明。** NFS/SMB 上 rename、fsync 的语义取决于服务端和挂载参数;对目录 fsync 不被支持时会静默跳过。
87
+ - **空白检查是文本启发式,不是语法分析。** 关键修改仍应看 diff、跑编译或测试。