@trim21/personal-pi-extensions 0.1.639 → 0.1.641
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/README.md +2 -2
- package/package.json +1 -1
- package/src/claude-code/files.ts +20 -6
- package/src/lib/lsp/rename-tool.ts +8 -1
- package/src/lib/write-guard.ts +51 -22
- package/src/opencode/files.ts +15 -2
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ bwrap 已集成进 bash 工具实现(opencode 风格 `bash` 位于 `src/openco
|
|
|
137
137
|
写保护直接内置在各写工具(opencode 风格 `write`/`edit`、Claude Code 风格 `Write`/`Edit`)内部,通过 `src/lib/write-guard.ts` 的 `guardWriteAccess` 实现。读取工具(`read`、`ls`、`find`、`grep`)不受限制。
|
|
138
138
|
|
|
139
139
|
- workspace 内或 `/tmp` 下的路径自动放行
|
|
140
|
-
- 外部路径需通过确认对话框由用户审批(**Approve once** / **Block** / **Block with reason**),对话框内以 ```diff
|
|
140
|
+
- 外部路径需通过确认对话框由用户审批(**Approve once** / **Block** / **Block with reason**),对话框内以 ```diff 代码块展示将要发生的变更预览(定位由各写工具注入自己的匹配实现,能定位时显示带行号的真实 patch,否则退化为参数 diff)
|
|
141
141
|
- headless(无 UI)会话直接拒绝外部写入;Windows 上不提供审批路径,工作区外写入一律拒绝
|
|
142
142
|
- `/bwrap-deny-request` 生效期间外部写入按无理由拒绝处理(`user deny <tool>: blocked`)
|
|
143
143
|
- 无需配置,随各写工具自动生效
|
|
@@ -253,7 +253,7 @@ opencode 风格工具集,随 `src/opencode/index.ts` 一次加载:`read` / `
|
|
|
253
253
|
|
|
254
254
|
### edit
|
|
255
255
|
|
|
256
|
-
`edit` 使用 [opencode](https://github.com/anomalyco/opencode) 的 schema 和模糊匹配引擎。核心 replacer 和 `replace()` 函数复制自 opencode,匹配引擎位于 `src/opencode/edit-engine.ts
|
|
256
|
+
`edit` 使用 [opencode](https://github.com/anomalyco/opencode) 的 schema 和模糊匹配引擎。核心 replacer 和 `replace()` 函数复制自 opencode,匹配引擎位于 `src/opencode/edit-engine.ts`;写保护审批弹窗(`src/lib/write-guard.ts`)的 diff 预览由调用方注入这套引擎,因此预览与落盘同语义。唯一的有意差异是去掉了原版的转义规范化(EscapeNormalizedReplacer):它会把源码里合法的 `\n`、`\t` 等转义序列当成模型多转义的产物做启发式反转义,可能改坏内容。
|
|
257
257
|
|
|
258
258
|
支持的匹配策略:
|
|
259
259
|
|
package/package.json
CHANGED
package/src/claude-code/files.ts
CHANGED
|
@@ -348,11 +348,25 @@ export function registerFileTools(
|
|
|
348
348
|
await guardWriteAccess(ctx, {
|
|
349
349
|
toolName: "Edit",
|
|
350
350
|
absolutePath: filePath,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
351
|
+
// 审批预览必须与落盘同语义:这里注入本工具自己的精确匹配(空 old_string
|
|
352
|
+
// 是创建/填充文件,按整文件写入预览)
|
|
353
|
+
change:
|
|
354
|
+
params.old_string === ""
|
|
355
|
+
? { kind: "write", newText: params.new_string }
|
|
356
|
+
: {
|
|
357
|
+
kind: "edit",
|
|
358
|
+
oldText: params.old_string,
|
|
359
|
+
newText: params.new_string,
|
|
360
|
+
apply: (content) => ({
|
|
361
|
+
contentOld: content,
|
|
362
|
+
contentNew: applyExactEdit(
|
|
363
|
+
content,
|
|
364
|
+
params.old_string,
|
|
365
|
+
params.new_string,
|
|
366
|
+
params.replace_all ?? false,
|
|
367
|
+
),
|
|
368
|
+
}),
|
|
369
|
+
},
|
|
356
370
|
policy,
|
|
357
371
|
signal,
|
|
358
372
|
});
|
|
@@ -521,7 +535,7 @@ export function registerFileTools(
|
|
|
521
535
|
await guardWriteAccess(ctx, {
|
|
522
536
|
toolName: "Write",
|
|
523
537
|
absolutePath: filePath,
|
|
524
|
-
change: {
|
|
538
|
+
change: { kind: "write", newText: params.content },
|
|
525
539
|
policy,
|
|
526
540
|
signal,
|
|
527
541
|
});
|
|
@@ -198,7 +198,14 @@ export function registerLspRenameTool(
|
|
|
198
198
|
await guardWriteAccess(ctx, {
|
|
199
199
|
toolName: "lsp-rename",
|
|
200
200
|
absolutePath: fileEdit.path,
|
|
201
|
-
change: {
|
|
201
|
+
change: {
|
|
202
|
+
kind: "edit",
|
|
203
|
+
oldText: fileEdit.oldText,
|
|
204
|
+
newText: fileEdit.newText,
|
|
205
|
+
// LSP 编辑已在内存里展开成整文件的 old/new,落盘是覆盖写:预览直接用
|
|
206
|
+
// 这一对文本,不必再把整份旧内容交给匹配引擎去找
|
|
207
|
+
apply: () => ({ contentOld: fileEdit.oldText, contentNew: fileEdit.newText }),
|
|
208
|
+
},
|
|
202
209
|
policy: options.policy,
|
|
203
210
|
signal,
|
|
204
211
|
});
|
package/src/lib/write-guard.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* outright, with the same error as the user picking "Block".
|
|
13
13
|
*
|
|
14
14
|
* Callers have already parsed their tool arguments, so the guard only takes the
|
|
15
|
-
* resolved pieces: the raw target path and the pending change
|
|
15
|
+
* resolved pieces: the raw target path and the pending change. An edit carries
|
|
16
|
+
* the caller's own matching implementation, so the previewed diff comes from the
|
|
17
|
+
* same semantics that will write the file.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import { readFile } from "node:fs/promises";
|
|
@@ -20,7 +22,6 @@ import { basename, isAbsolute, relative, sep } from "node:path";
|
|
|
20
22
|
|
|
21
23
|
import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
22
24
|
|
|
23
|
-
import { applyEdit, normalizeToLF } from "../opencode/edit-engine.js";
|
|
24
25
|
import { fenceCodeBlock } from "./markdown.js";
|
|
25
26
|
import type { RequestPolicy } from "./request-policy.js";
|
|
26
27
|
|
|
@@ -57,14 +58,46 @@ function wrapDiff(patch: string): string {
|
|
|
57
58
|
return fenceCodeBlock(body, "diff");
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* 审批预览用的应用函数:把这次改动套到文件内容上,返回 diff 用的前后文本。
|
|
63
|
+
* 必须是该工具落盘时使用的同一套匹配实现——否则对话框展示的 diff 与实际写入
|
|
64
|
+
* 可能不一致。
|
|
65
|
+
*/
|
|
66
|
+
export type PendingChangeApply = (fileContent: string) => {
|
|
67
|
+
readonly contentOld: string;
|
|
68
|
+
readonly contentNew: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** 整文件写入:预览是完整的增删 patch,不需要匹配实现。 */
|
|
72
|
+
export interface FileWriteChange {
|
|
73
|
+
readonly kind: "write";
|
|
74
|
+
readonly newText: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 替换:`apply` 是调用方自己的匹配实现(精确匹配、模糊匹配,或已展开好的整文件
|
|
79
|
+
* 编辑)。匹配不上时抛错即可,写保护会退化为参数 diff。
|
|
80
|
+
*/
|
|
81
|
+
export interface FileEditChange {
|
|
82
|
+
readonly kind: "edit";
|
|
83
|
+
readonly oldText: string;
|
|
84
|
+
readonly newText: string;
|
|
85
|
+
readonly apply: PendingChangeApply;
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
/** The pending file change, described by the caller from already-parsed args. */
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
export type PendingChange = FileWriteChange | FileEditChange;
|
|
90
|
+
|
|
91
|
+
/** 预览统一按 LF 渲染:CRLF 文件的 diff 每行都带 \r,在对话框里显示成乱码。 */
|
|
92
|
+
function normalizeToLF(text: string): string {
|
|
93
|
+
return text.replaceAll("\r\n", "\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 参数 diff:调用方的匹配实现报告不出结果时的兜底形式。 */
|
|
97
|
+
function parameterDiff(oldText: string, newText: string): string {
|
|
98
|
+
const removed = oldText.split("\n").map((line) => `-${line}`);
|
|
99
|
+
const added = newText.split("\n").map((line) => `+${line}`);
|
|
100
|
+
return [...removed, ...added].join("\n");
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
/**
|
|
@@ -75,35 +108,31 @@ export async function buildDiffPreview(
|
|
|
75
108
|
resolvedPath: string,
|
|
76
109
|
change: PendingChange,
|
|
77
110
|
): Promise<string | undefined> {
|
|
78
|
-
let
|
|
111
|
+
let fileContent = "";
|
|
79
112
|
try {
|
|
80
|
-
|
|
113
|
+
fileContent = await readFile(resolvedPath, "utf8");
|
|
81
114
|
} catch {
|
|
82
115
|
// Unreadable or missing file: treat as empty so writes show as full additions.
|
|
83
116
|
}
|
|
84
117
|
|
|
85
|
-
if (change.
|
|
118
|
+
if (change.kind === "write") {
|
|
86
119
|
// Whole-file write: show the full addition/replacement patch.
|
|
87
|
-
return wrapDiff(generateUnifiedPatch(basename(resolvedPath),
|
|
120
|
+
return wrapDiff(generateUnifiedPatch(basename(resolvedPath), fileContent, change.newText, 2));
|
|
88
121
|
}
|
|
89
122
|
|
|
90
|
-
//
|
|
91
|
-
// line-numbered patch when it matches. Fall back to a parameter diff when the
|
|
92
|
-
// edit cannot be applied (oldText not found, ambiguous, or no file).
|
|
123
|
+
// 定位由调用方的匹配实现负责(与其落盘同语义);匹配不上时退回参数 diff。
|
|
93
124
|
try {
|
|
94
|
-
const
|
|
125
|
+
const { contentOld, contentNew } = change.apply(fileContent);
|
|
95
126
|
return wrapDiff(
|
|
96
127
|
generateUnifiedPatch(
|
|
97
128
|
basename(resolvedPath),
|
|
98
|
-
normalizeToLF(
|
|
99
|
-
normalizeToLF(
|
|
129
|
+
normalizeToLF(contentOld),
|
|
130
|
+
normalizeToLF(contentNew),
|
|
100
131
|
2,
|
|
101
132
|
),
|
|
102
133
|
);
|
|
103
134
|
} catch {
|
|
104
|
-
|
|
105
|
-
const added = change.newText.split("\n").map((line) => `+${line}`);
|
|
106
|
-
return wrapDiff([...removed, ...added].join("\n"));
|
|
135
|
+
return wrapDiff(parameterDiff(change.oldText, change.newText));
|
|
107
136
|
}
|
|
108
137
|
}
|
|
109
138
|
|
package/src/opencode/files.ts
CHANGED
|
@@ -635,7 +635,20 @@ function registerEditTool(
|
|
|
635
635
|
await guardWriteAccess(ctx, {
|
|
636
636
|
toolName: "edit",
|
|
637
637
|
absolutePath,
|
|
638
|
-
|
|
638
|
+
// 审批预览必须与落盘同语义:注入本工具自己的模糊匹配引擎(空 oldString
|
|
639
|
+
// 是创建新文件,按整文件写入预览)
|
|
640
|
+
change:
|
|
641
|
+
oldString === ""
|
|
642
|
+
? { kind: "write", newText: newString }
|
|
643
|
+
: {
|
|
644
|
+
kind: "edit",
|
|
645
|
+
oldText: oldString,
|
|
646
|
+
newText: newString,
|
|
647
|
+
apply: (content) => {
|
|
648
|
+
const applied = applyEdit(content, oldString, newString, replaceAll);
|
|
649
|
+
return { contentOld: applied.contentOld, contentNew: applied.contentNew };
|
|
650
|
+
},
|
|
651
|
+
},
|
|
639
652
|
policy,
|
|
640
653
|
signal,
|
|
641
654
|
});
|
|
@@ -800,7 +813,7 @@ function registerWriteTool(
|
|
|
800
813
|
await guardWriteAccess(ctx, {
|
|
801
814
|
toolName: "write",
|
|
802
815
|
absolutePath,
|
|
803
|
-
change: {
|
|
816
|
+
change: { kind: "write", newText: content },
|
|
804
817
|
policy,
|
|
805
818
|
signal,
|
|
806
819
|
});
|