pi-better-btw-plus 1.0.0

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.
@@ -0,0 +1,225 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import type { FileActivityTracker } from "./file-activity-tracker.ts";
3
+
4
+ export function wrapToolsWithOverlapDetection(
5
+ tools: AgentTool[],
6
+ tracker: FileActivityTracker,
7
+ cwd: string,
8
+ confirmOverlap: (path: string) => Promise<boolean>,
9
+ ): AgentTool[] {
10
+ const writingTools = ["write", "edit", "bash"];
11
+ return tools.map((tool) =>
12
+ writingTools.includes(tool.name) ? wrapTool(tool, tracker, cwd, confirmOverlap) : tool
13
+ );
14
+ }
15
+
16
+ function wrapTool(
17
+ tool: AgentTool,
18
+ tracker: FileActivityTracker,
19
+ cwd: string,
20
+ confirmOverlap: (path: string) => Promise<boolean>,
21
+ ): AgentTool {
22
+ return {
23
+ ...tool,
24
+ execute: async (toolCallId, args, signal, onUpdate) => {
25
+ const paths = extractWritePaths(tool.name, args);
26
+
27
+ for (const path of paths) {
28
+ if (tracker.hasWritten(path, cwd)) {
29
+ const proceed = await confirmOverlap(path);
30
+ if (!proceed) {
31
+ return {
32
+ content: [{ type: "text", text: `Skipped: ${path} (main agent has modified it)` }],
33
+ details: undefined,
34
+ };
35
+ }
36
+ }
37
+ }
38
+
39
+ return tool.execute(toolCallId, args, signal, onUpdate);
40
+ },
41
+ };
42
+ }
43
+
44
+ export function extractWritePaths(toolName: string, args: unknown): string[] {
45
+ const record = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
46
+
47
+ switch (toolName) {
48
+ case "write":
49
+ case "edit":
50
+ return typeof record.path === "string" ? [record.path] : [];
51
+ case "bash":
52
+ return typeof record.command === "string" ? parseBashWritePaths(record.command) : [];
53
+ default:
54
+ return [];
55
+ }
56
+ }
57
+
58
+ function parseBashWritePaths(command: string): string[] {
59
+ const tokens = tokenizeShell(command);
60
+ const paths: string[] = [];
61
+ let segment: ShellToken[] = [];
62
+
63
+ for (const token of tokens) {
64
+ if (token.type === "op" && isCommandSeparator(token.value)) {
65
+ collectSegmentWritePaths(segment, paths);
66
+ segment = [];
67
+ continue;
68
+ }
69
+ segment.push(token);
70
+ }
71
+
72
+ collectSegmentWritePaths(segment, paths);
73
+ return [...new Set(paths)];
74
+ }
75
+
76
+ type ShellToken =
77
+ | { type: "word"; value: string }
78
+ | { type: "op"; value: ">" | ">>" | "|" | "||" | "&" | "&&" | ";" };
79
+
80
+ function tokenizeShell(command: string): ShellToken[] {
81
+ const tokens: ShellToken[] = [];
82
+
83
+ for (let i = 0; i < command.length;) {
84
+ const char = command[i];
85
+
86
+ if (/\s/.test(char)) {
87
+ i++;
88
+ continue;
89
+ }
90
+
91
+ const twoCharOp = command.slice(i, i + 2);
92
+ if (twoCharOp === ">>" || twoCharOp === "||" || twoCharOp === "&&") {
93
+ tokens.push({ type: "op", value: twoCharOp });
94
+ i += 2;
95
+ continue;
96
+ }
97
+
98
+ if (char === ">" || char === "|" || char === "&" || char === ";") {
99
+ tokens.push({ type: "op", value: char });
100
+ i++;
101
+ continue;
102
+ }
103
+
104
+ let value = "";
105
+ while (i < command.length) {
106
+ const current = command[i];
107
+
108
+ if (/\s/.test(current) || current === ">" || current === "|" || current === "&" || current === ";") {
109
+ break;
110
+ }
111
+
112
+ if (current === "\\") {
113
+ if (i + 1 < command.length) {
114
+ value += command[i + 1];
115
+ i += 2;
116
+ } else {
117
+ i++;
118
+ }
119
+ continue;
120
+ }
121
+
122
+ if (current === "'") {
123
+ i++;
124
+ while (i < command.length && command[i] !== "'") {
125
+ value += command[i];
126
+ i++;
127
+ }
128
+ if (command[i] === "'") i++;
129
+ continue;
130
+ }
131
+
132
+ if (current === '"') {
133
+ i++;
134
+ while (i < command.length && command[i] !== '"') {
135
+ if (command[i] === "\\" && i + 1 < command.length && /["\\$`]/.test(command[i + 1])) {
136
+ value += command[i + 1];
137
+ i += 2;
138
+ } else {
139
+ value += command[i];
140
+ i++;
141
+ }
142
+ }
143
+ if (command[i] === '"') i++;
144
+ continue;
145
+ }
146
+
147
+ value += current;
148
+ i++;
149
+ }
150
+
151
+ if (value) tokens.push({ type: "word", value });
152
+ }
153
+
154
+ return tokens;
155
+ }
156
+
157
+ function collectSegmentWritePaths(segment: ShellToken[], paths: string[]): void {
158
+ for (let i = 0; i < segment.length; i++) {
159
+ const token = segment[i];
160
+ if (token.type === "op" && (token.value === ">" || token.value === ">>")) {
161
+ if (segment[i + 1]?.type === "word") {
162
+ pushPath(paths, segment[i + 1].value);
163
+ i++;
164
+ }
165
+ }
166
+ }
167
+
168
+ const commandIndex = segment.findIndex((token) => token.type === "word");
169
+ if (commandIndex === -1) return;
170
+
171
+ const command = segment[commandIndex];
172
+
173
+ const operands = collectCommandOperands(segment.slice(commandIndex + 1));
174
+ if (command.value === "tee" || command.value === "touch" || command.value === "rm") {
175
+ for (const operand of operands) pushPath(paths, operand);
176
+ }
177
+ if ((command.value === "cp" || command.value === "mv") && operands.length >= 2) {
178
+ pushPath(paths, operands[operands.length - 1]);
179
+ }
180
+ }
181
+
182
+ function collectCommandOperands(tokens: ShellToken[]): string[] {
183
+ const operands: string[] = [];
184
+ let parsingOptions = true;
185
+
186
+ for (let i = 0; i < tokens.length; i++) {
187
+ const token = tokens[i];
188
+
189
+ if (token.type === "op") {
190
+ if (token.value === ">" || token.value === ">>") {
191
+ if (tokens[i + 1]?.type === "word") i++;
192
+ }
193
+ continue;
194
+ }
195
+
196
+ if (parsingOptions) {
197
+ if (token.value === "--") {
198
+ parsingOptions = false;
199
+ continue;
200
+ }
201
+ if (token.value.startsWith("-")) {
202
+ continue;
203
+ }
204
+ parsingOptions = false;
205
+ }
206
+
207
+ operands.push(token.value);
208
+ }
209
+
210
+ return operands;
211
+ }
212
+
213
+ function isCommandSeparator(value: ShellToken["value"]): boolean {
214
+ return value === "|" || value === "||" || value === "&" || value === "&&" || value === ";";
215
+ }
216
+
217
+ function pushPath(paths: string[], path: string): void {
218
+ if (path && !path.startsWith("-") && !isIgnoredWritePath(path)) {
219
+ paths.push(path);
220
+ }
221
+ }
222
+
223
+ function isIgnoredWritePath(path: string): boolean {
224
+ return path === "/dev/null" || path === "/dev/stdout" || path === "/dev/stderr" || path.startsWith("/dev/fd/");
225
+ }