@bdsqqq/pi 0.0.1
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/dist/chunk-DsIazq42.js +26 -0
- package/dist/command-palette-B97yKqne.js +406 -0
- package/dist/core/agents-md.d.ts +22 -0
- package/dist/core/agents-md.js +174 -0
- package/dist/core/box-chrome.d.ts +56 -0
- package/dist/core/box-chrome.js +207 -0
- package/dist/core/box-format.d.ts +82 -0
- package/dist/core/box-format.js +381 -0
- package/dist/core/config.d.ts +14 -0
- package/dist/core/config.js +201 -0
- package/dist/core/file-tracker.d.ts +65 -0
- package/dist/core/file-tracker.js +439 -0
- package/dist/core/github-api.d.ts +45 -0
- package/dist/core/github-api.js +101 -0
- package/dist/core/html-to-md.d.ts +5 -0
- package/dist/core/html-to-md.js +289 -0
- package/dist/core/interpolate.d.ts +44 -0
- package/dist/core/interpolate.js +475 -0
- package/dist/core/mutex.d.ts +8 -0
- package/dist/core/mutex.js +142 -0
- package/dist/core/output-buffer.d.ts +80 -0
- package/dist/core/output-buffer.js +517 -0
- package/dist/core/permissions.d.ts +19 -0
- package/dist/core/permissions.js +140 -0
- package/dist/core/pi-spawn.d.ts +62 -0
- package/dist/core/pi-spawn.js +244 -0
- package/dist/core/prompt-patch.d.ts +11 -0
- package/dist/core/prompt-patch.js +16 -0
- package/dist/core/show-renderer.d.ts +10 -0
- package/dist/core/show-renderer.js +39 -0
- package/dist/core/show.d.ts +29 -0
- package/dist/core/show.js +288 -0
- package/dist/core/sub-agent-render.d.ts +56 -0
- package/dist/core/sub-agent-render.js +579 -0
- package/dist/core/tool-cost.d.ts +17 -0
- package/dist/core/tool-cost.js +58 -0
- package/dist/core/tui.d.ts +9 -0
- package/dist/core/tui.js +33 -0
- package/dist/editor-BljciPdx.js +537 -0
- package/dist/extensions/bash.d.ts +12 -0
- package/dist/extensions/bash.js +261 -0
- package/dist/extensions/code-review.d.ts +14 -0
- package/dist/extensions/code-review.js +175 -0
- package/dist/extensions/command-palette.d.ts +6 -0
- package/dist/extensions/command-palette.js +2 -0
- package/dist/extensions/create-file.d.ts +7 -0
- package/dist/extensions/create-file.js +88 -0
- package/dist/extensions/edit-file.d.ts +7 -0
- package/dist/extensions/edit-file.js +395 -0
- package/dist/extensions/editor.d.ts +6 -0
- package/dist/extensions/editor.js +2 -0
- package/dist/extensions/finder.d.ts +13 -0
- package/dist/extensions/finder.js +108 -0
- package/dist/extensions/format-file.d.ts +11 -0
- package/dist/extensions/format-file.js +145 -0
- package/dist/extensions/github.d.ts +13 -0
- package/dist/extensions/github.js +527 -0
- package/dist/extensions/glob.d.ts +10 -0
- package/dist/extensions/glob.js +163 -0
- package/dist/extensions/grep.d.ts +13 -0
- package/dist/extensions/grep.js +356 -0
- package/dist/extensions/handoff.d.ts +6 -0
- package/dist/extensions/handoff.js +277 -0
- package/dist/extensions/librarian.d.ts +13 -0
- package/dist/extensions/librarian.js +113 -0
- package/dist/extensions/look-at.d.ts +13 -0
- package/dist/extensions/look-at.js +121 -0
- package/dist/extensions/ls.d.ts +8 -0
- package/dist/extensions/ls.js +87 -0
- package/dist/extensions/mermaid.d.ts +20 -0
- package/dist/extensions/mermaid.js +2 -0
- package/dist/extensions/oracle.d.ts +13 -0
- package/dist/extensions/oracle.js +143 -0
- package/dist/extensions/read-session.d.ts +13 -0
- package/dist/extensions/read-session.js +265 -0
- package/dist/extensions/read-web-page.d.ts +11 -0
- package/dist/extensions/read-web-page.js +234 -0
- package/dist/extensions/read.d.ts +23 -0
- package/dist/extensions/read.js +323 -0
- package/dist/extensions/search-sessions.d.ts +29 -0
- package/dist/extensions/search-sessions.js +426 -0
- package/dist/extensions/session-name.d.ts +6 -0
- package/dist/extensions/session-name.js +54 -0
- package/dist/extensions/skill.d.ts +7 -0
- package/dist/extensions/skill.js +232 -0
- package/dist/extensions/system-prompt.d.ts +6 -0
- package/dist/extensions/system-prompt.js +31 -0
- package/dist/extensions/task.d.ts +11 -0
- package/dist/extensions/task.js +114 -0
- package/dist/extensions/tool-harness.d.ts +6 -0
- package/dist/extensions/tool-harness.js +37 -0
- package/dist/extensions/undo-edit.d.ts +7 -0
- package/dist/extensions/undo-edit.js +127 -0
- package/dist/extensions/web-search.d.ts +12 -0
- package/dist/extensions/web-search.js +254 -0
- package/dist/extensions.d.ts +29 -0
- package/dist/extensions.js +29 -0
- package/dist/mermaid-C7xCVBrw.js +95296 -0
- package/package.json +67 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { withPromptPatch } from "../core/prompt-patch.js";
|
|
2
|
+
import { formatBoxesWindowed, osc8Link } from "../core/box-format.js";
|
|
3
|
+
import { withFileLock } from "../core/mutex.js";
|
|
4
|
+
import { resolveWithVariants } from "./read.js";
|
|
5
|
+
import { saveChange, simpleDiff } from "../core/file-tracker.js";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
9
|
+
import { Type } from "@sinclair/typebox";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
//#region packages/extensions/edit-file/index.ts
|
|
12
|
+
/**
|
|
13
|
+
* edit tool — replaces pi's built-in with enhanced file editing.
|
|
14
|
+
*
|
|
15
|
+
* differences from pi's built-in:
|
|
16
|
+
* - mutex-locked per file path (prevents partial writes from concurrent edits)
|
|
17
|
+
* - replace_all mode for multiple occurrences
|
|
18
|
+
* - escape sequence fallback (\n, \t when exact match fails)
|
|
19
|
+
* - redaction check (rejects edits introducing placeholder markers)
|
|
20
|
+
* - file change tracking for undo_edit via lib/file-tracker
|
|
21
|
+
* - BOM/CRLF preservation
|
|
22
|
+
*
|
|
23
|
+
* shadows pi's built-in `edit` tool via same-name registration.
|
|
24
|
+
* uses model-compatible parameter names (old_str, new_str, replace_all)
|
|
25
|
+
* rather than pi's (oldText, newText) — models produce these param
|
|
26
|
+
* names naturally.
|
|
27
|
+
*/
|
|
28
|
+
function stripBom(content) {
|
|
29
|
+
return content.startsWith("") ? {
|
|
30
|
+
bom: "",
|
|
31
|
+
text: content.slice(1)
|
|
32
|
+
} : {
|
|
33
|
+
bom: "",
|
|
34
|
+
text: content
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function detectLineEnding(content) {
|
|
38
|
+
const crlfIdx = content.indexOf("\r\n");
|
|
39
|
+
const lfIdx = content.indexOf("\n");
|
|
40
|
+
if (lfIdx === -1 || crlfIdx === -1) return "\n";
|
|
41
|
+
return crlfIdx < lfIdx ? "\r\n" : "\n";
|
|
42
|
+
}
|
|
43
|
+
function normalizeToLF(text) {
|
|
44
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
45
|
+
}
|
|
46
|
+
function restoreLineEndings(text, ending) {
|
|
47
|
+
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* LLMs sometimes emit literal \n / \t in JSON strings when they mean
|
|
51
|
+
* actual whitespace. the JSON parser produces backslash + letter, not
|
|
52
|
+
* a real newline. this function converts those back.
|
|
53
|
+
*/
|
|
54
|
+
function unescapeStr(s) {
|
|
55
|
+
return s.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\r/g, "\r").replace(/\\\\/g, "\\");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* trailing whitespace + unicode normalization.
|
|
59
|
+
* mirrors pi's edit-diff.ts normalizeForFuzzyMatch so we get the same
|
|
60
|
+
* fallback behavior the model is used to from the built-in tool.
|
|
61
|
+
*/
|
|
62
|
+
function normalizeForFuzzy(text) {
|
|
63
|
+
return text.split("\n").map((line) => line.trimEnd()).join("\n").replace(/[\u2018\u2019\u201A\u201B]/g, "'").replace(/[\u201C\u201D\u201E\u201F]/g, "\"").replace(/[\u2010-\u2015\u2212]/g, "-").replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 3-tier matching: exact → unescaped → fuzzy-normalized.
|
|
67
|
+
* returns the content variant + effective search/replace strings so the
|
|
68
|
+
* caller can apply the replacement in the correct text space.
|
|
69
|
+
*/
|
|
70
|
+
function findMatchStrategy(content, oldStr, newStr) {
|
|
71
|
+
let idx = content.indexOf(oldStr);
|
|
72
|
+
if (idx !== -1) return {
|
|
73
|
+
searchStr: oldStr,
|
|
74
|
+
replaceStr: newStr,
|
|
75
|
+
content,
|
|
76
|
+
index: idx,
|
|
77
|
+
matchLength: oldStr.length
|
|
78
|
+
};
|
|
79
|
+
const unescOld = unescapeStr(oldStr);
|
|
80
|
+
const unescNew = unescapeStr(newStr);
|
|
81
|
+
if (unescOld !== oldStr) {
|
|
82
|
+
idx = content.indexOf(unescOld);
|
|
83
|
+
if (idx !== -1) return {
|
|
84
|
+
searchStr: unescOld,
|
|
85
|
+
replaceStr: unescNew,
|
|
86
|
+
content,
|
|
87
|
+
index: idx,
|
|
88
|
+
matchLength: unescOld.length
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const fuzzyContent = normalizeForFuzzy(content);
|
|
92
|
+
const fuzzyOld = normalizeForFuzzy(oldStr);
|
|
93
|
+
idx = fuzzyContent.indexOf(fuzzyOld);
|
|
94
|
+
if (idx !== -1) return {
|
|
95
|
+
searchStr: fuzzyOld,
|
|
96
|
+
replaceStr: newStr,
|
|
97
|
+
content: fuzzyContent,
|
|
98
|
+
index: idx,
|
|
99
|
+
matchLength: fuzzyOld.length
|
|
100
|
+
};
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function countOccurrences(content, searchStr) {
|
|
104
|
+
if (searchStr.length === 0) return 0;
|
|
105
|
+
let count = 0;
|
|
106
|
+
let pos = 0;
|
|
107
|
+
while ((pos = content.indexOf(searchStr, pos)) !== -1) {
|
|
108
|
+
count++;
|
|
109
|
+
pos += searchStr.length;
|
|
110
|
+
}
|
|
111
|
+
return count;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* patterns that indicate the LLM replaced real content with a placeholder.
|
|
115
|
+
* checked against new_str only when the pattern is absent from old_str
|
|
116
|
+
* (so legitimate test strings containing these phrases pass through).
|
|
117
|
+
*/
|
|
118
|
+
const REDACTION_PATTERNS = [
|
|
119
|
+
/\[REDACTED\]/i,
|
|
120
|
+
/\[\.\.\.omitted.*?\]/i,
|
|
121
|
+
/\[rest of .{1,40} unchanged\]/i,
|
|
122
|
+
/\[remaining .{1,40} unchanged\]/i,
|
|
123
|
+
/\/\/ \.\.\.( rest| remaining)? (of )?(the )?(file|code|content|implementation)( remains?)? (unchanged|the same|as before|omitted)/i,
|
|
124
|
+
/\/\/ \.\.\. existing (code|content|implementation)/i,
|
|
125
|
+
/# \.\.\. existing (code|content|implementation)/i
|
|
126
|
+
];
|
|
127
|
+
function hasNewRedactionMarkers(oldStr, newStr) {
|
|
128
|
+
for (const pattern of REDACTION_PATTERNS) if (pattern.test(newStr) && !pattern.test(oldStr)) return newStr.match(pattern)?.[0] ?? "redaction marker";
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* parse unified diff output into BoxSection[] for box-format rendering.
|
|
133
|
+
* each hunk becomes a BoxBlock. +/- lines are highlighted, context lines dim.
|
|
134
|
+
*/
|
|
135
|
+
function parseDiffToSections(filename, diffText) {
|
|
136
|
+
const lines = diffText.split("\n");
|
|
137
|
+
const blocks = [];
|
|
138
|
+
let currentLines = [];
|
|
139
|
+
let oldLine = 0;
|
|
140
|
+
let newLine = 0;
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
if (line.startsWith("--- ") || line.startsWith("+++ ")) continue;
|
|
143
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
144
|
+
if (hunkMatch && hunkMatch[1] && hunkMatch[2]) {
|
|
145
|
+
if (currentLines.length > 0) {
|
|
146
|
+
blocks.push({ lines: currentLines });
|
|
147
|
+
currentLines = [];
|
|
148
|
+
}
|
|
149
|
+
oldLine = parseInt(hunkMatch[1], 10);
|
|
150
|
+
newLine = parseInt(hunkMatch[2], 10);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (line.startsWith("-")) {
|
|
154
|
+
currentLines.push({
|
|
155
|
+
gutter: String(oldLine),
|
|
156
|
+
text: line,
|
|
157
|
+
highlight: true
|
|
158
|
+
});
|
|
159
|
+
oldLine++;
|
|
160
|
+
} else if (line.startsWith("+")) {
|
|
161
|
+
currentLines.push({
|
|
162
|
+
gutter: String(newLine),
|
|
163
|
+
text: line,
|
|
164
|
+
highlight: true
|
|
165
|
+
});
|
|
166
|
+
newLine++;
|
|
167
|
+
} else {
|
|
168
|
+
currentLines.push({
|
|
169
|
+
gutter: String(oldLine),
|
|
170
|
+
text: line,
|
|
171
|
+
highlight: false
|
|
172
|
+
});
|
|
173
|
+
oldLine++;
|
|
174
|
+
newLine++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (currentLines.length > 0) blocks.push({ lines: currentLines });
|
|
178
|
+
return [{
|
|
179
|
+
header: filename,
|
|
180
|
+
blocks
|
|
181
|
+
}];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* compute +added/~modified/-removed from diff lines.
|
|
185
|
+
* adjacent - then + blocks are paired as modifications;
|
|
186
|
+
* the min count is ~modified, excess is pure +/-.
|
|
187
|
+
*/
|
|
188
|
+
function computeDiffStats(sections) {
|
|
189
|
+
let added = 0;
|
|
190
|
+
let removed = 0;
|
|
191
|
+
let modified = 0;
|
|
192
|
+
for (const section of sections) for (const block of section.blocks) {
|
|
193
|
+
let i = 0;
|
|
194
|
+
while (i < block.lines.length) {
|
|
195
|
+
const line = block.lines[i];
|
|
196
|
+
if (!line) {
|
|
197
|
+
i++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (line.text.startsWith("-")) {
|
|
201
|
+
let delCount = 0;
|
|
202
|
+
while (i < block.lines.length && block.lines[i]?.text.startsWith("-")) {
|
|
203
|
+
delCount++;
|
|
204
|
+
i++;
|
|
205
|
+
}
|
|
206
|
+
let addCount = 0;
|
|
207
|
+
while (i < block.lines.length && block.lines[i]?.text.startsWith("+")) {
|
|
208
|
+
addCount++;
|
|
209
|
+
i++;
|
|
210
|
+
}
|
|
211
|
+
const paired = Math.min(delCount, addCount);
|
|
212
|
+
modified += paired;
|
|
213
|
+
removed += delCount - paired;
|
|
214
|
+
added += addCount - paired;
|
|
215
|
+
} else if (line.text.startsWith("+")) {
|
|
216
|
+
added++;
|
|
217
|
+
i++;
|
|
218
|
+
} else i++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
added,
|
|
223
|
+
removed,
|
|
224
|
+
modified
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function formatStats(stats, theme) {
|
|
228
|
+
const parts = [];
|
|
229
|
+
if (stats.added > 0) parts.push(theme.fg("toolDiffAdded", `+${stats.added}`));
|
|
230
|
+
if (stats.modified > 0) parts.push(theme.fg("warning", `~${stats.modified}`));
|
|
231
|
+
if (stats.removed > 0) parts.push(theme.fg("toolDiffRemoved", `-${stats.removed}`));
|
|
232
|
+
return parts.length > 0 ? parts.join(" ") : theme.fg("dim", "no changes");
|
|
233
|
+
}
|
|
234
|
+
function createEditFileTool() {
|
|
235
|
+
return {
|
|
236
|
+
name: "edit",
|
|
237
|
+
label: "Edit File",
|
|
238
|
+
description: "Make edits to a text file.\n\nReplaces `old_str` with `new_str` in the given file.\n\nReturns a diff showing the changes made.\n\nThe file specified by `path` MUST exist.\n\n`old_str` MUST exist in the file.\n\n`old_str` and `new_str` MUST be different from each other.\n\nSet `replace_all` to true to replace all occurrences of `old_str` in the file. Otherwise, `old_str` MUST be unique within the file or the edit will fail. Additional lines of context can be added to make the string more unique.\n\nWhen changing an existing file, use this tool. Only use the write tool for files that do not exist yet.",
|
|
239
|
+
parameters: Type.Object({
|
|
240
|
+
path: Type.String({ description: "The absolute path to the file (MUST be absolute, not relative). File must exist." }),
|
|
241
|
+
old_str: Type.String({ description: "Text to search for. Must match exactly." }),
|
|
242
|
+
new_str: Type.String({ description: "Text to replace old_str with." }),
|
|
243
|
+
replace_all: Type.Optional(Type.Boolean({
|
|
244
|
+
description: "Set to true to replace all occurrences of old_str. Otherwise, old_str must be unique.",
|
|
245
|
+
default: false
|
|
246
|
+
}))
|
|
247
|
+
}),
|
|
248
|
+
renderCall(args, theme) {
|
|
249
|
+
const filePath = args.path || "...";
|
|
250
|
+
const home = os.homedir();
|
|
251
|
+
const shortened = filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
|
|
252
|
+
const linked = filePath.startsWith("/") ? osc8Link(`file://${filePath}`, shortened) : shortened;
|
|
253
|
+
return new Text(theme.fg("toolTitle", theme.bold("Edit ")) + theme.fg("dim", linked), 0, 0);
|
|
254
|
+
},
|
|
255
|
+
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
256
|
+
const p = params;
|
|
257
|
+
const resolved = resolveWithVariants(p.path, ctx.cwd);
|
|
258
|
+
if (!fs.existsSync(resolved)) return {
|
|
259
|
+
content: [{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: `file not found: ${resolved}`
|
|
262
|
+
}],
|
|
263
|
+
isError: true
|
|
264
|
+
};
|
|
265
|
+
if (fs.statSync(resolved).isDirectory()) return {
|
|
266
|
+
content: [{
|
|
267
|
+
type: "text",
|
|
268
|
+
text: `${resolved} is a directory, not a file.`
|
|
269
|
+
}],
|
|
270
|
+
isError: true
|
|
271
|
+
};
|
|
272
|
+
const redactionMarker = hasNewRedactionMarkers(p.old_str, p.new_str);
|
|
273
|
+
if (redactionMarker) return {
|
|
274
|
+
content: [{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: `rejected: new_str contains a redaction marker ("${redactionMarker}"). provide the actual content instead of placeholders.`
|
|
277
|
+
}],
|
|
278
|
+
isError: true
|
|
279
|
+
};
|
|
280
|
+
return withFileLock(resolved, async () => {
|
|
281
|
+
const rawContent = fs.readFileSync(resolved, "utf-8");
|
|
282
|
+
const { bom, text: bomStripped } = stripBom(rawContent);
|
|
283
|
+
const originalEnding = detectLineEnding(bomStripped);
|
|
284
|
+
const normalized = normalizeToLF(bomStripped);
|
|
285
|
+
const oldStr = normalizeToLF(p.old_str);
|
|
286
|
+
const newStr = normalizeToLF(p.new_str);
|
|
287
|
+
if (oldStr === newStr) return {
|
|
288
|
+
content: [{
|
|
289
|
+
type: "text",
|
|
290
|
+
text: "old_str and new_str are identical. no changes needed."
|
|
291
|
+
}],
|
|
292
|
+
isError: true
|
|
293
|
+
};
|
|
294
|
+
const strategy = findMatchStrategy(normalized, oldStr, newStr);
|
|
295
|
+
if (!strategy) return {
|
|
296
|
+
content: [{
|
|
297
|
+
type: "text",
|
|
298
|
+
text: `could not find old_str in ${path.basename(resolved)}. the text must match exactly including whitespace and newlines.`
|
|
299
|
+
}],
|
|
300
|
+
isError: true
|
|
301
|
+
};
|
|
302
|
+
const occurrences = countOccurrences(strategy.content, strategy.searchStr);
|
|
303
|
+
const replaceAll = p.replace_all ?? false;
|
|
304
|
+
if (!replaceAll && occurrences > 1) return {
|
|
305
|
+
content: [{
|
|
306
|
+
type: "text",
|
|
307
|
+
text: `found ${occurrences} occurrences of old_str in ${path.basename(resolved)}. set replace_all to true, or add more context to make the match unique.`
|
|
308
|
+
}],
|
|
309
|
+
isError: true
|
|
310
|
+
};
|
|
311
|
+
let newContent;
|
|
312
|
+
if (replaceAll) newContent = strategy.content.split(strategy.searchStr).join(strategy.replaceStr);
|
|
313
|
+
else newContent = strategy.content.substring(0, strategy.index) + strategy.replaceStr + strategy.content.substring(strategy.index + strategy.matchLength);
|
|
314
|
+
if (strategy.content === newContent) return {
|
|
315
|
+
content: [{
|
|
316
|
+
type: "text",
|
|
317
|
+
text: "no changes made — replacement produced identical content."
|
|
318
|
+
}],
|
|
319
|
+
isError: true
|
|
320
|
+
};
|
|
321
|
+
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
|
322
|
+
fs.writeFileSync(resolved, finalContent, "utf-8");
|
|
323
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
324
|
+
const trackingDiff = simpleDiff(resolved, rawContent, finalContent);
|
|
325
|
+
saveChange(sessionId, toolCallId, {
|
|
326
|
+
uri: `file://${resolved}`,
|
|
327
|
+
before: rawContent,
|
|
328
|
+
after: finalContent,
|
|
329
|
+
diff: trackingDiff,
|
|
330
|
+
isNewFile: false,
|
|
331
|
+
timestamp: Date.now()
|
|
332
|
+
});
|
|
333
|
+
return {
|
|
334
|
+
content: [{
|
|
335
|
+
type: "text",
|
|
336
|
+
text: simpleDiff(resolved, strategy.content, newContent)
|
|
337
|
+
}],
|
|
338
|
+
details: {
|
|
339
|
+
filePath: resolved,
|
|
340
|
+
...replaceAll && occurrences > 1 ? { replaceCount: occurrences } : {}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
},
|
|
345
|
+
renderResult(result, { expanded }, theme) {
|
|
346
|
+
const content = result.content?.[0];
|
|
347
|
+
if (!content || content.type !== "text") return new Text(theme.fg("dim", "(no output)"), 0, 0);
|
|
348
|
+
const diffText = content.text;
|
|
349
|
+
const filename = diffText.match(/^---\s+(\S+)/m)?.[1] ?? "file";
|
|
350
|
+
const filePath = result.details?.filePath;
|
|
351
|
+
const sections = parseDiffToSections(filename, diffText);
|
|
352
|
+
if (!sections?.length || !sections[0]?.blocks.length) return new Text(theme.fg("dim", "(no changes)"), 0, 0);
|
|
353
|
+
const statsText = formatStats(computeDiffStats(sections), theme);
|
|
354
|
+
const replaceCount = result.details?.replaceCount;
|
|
355
|
+
const replaceNote = replaceCount && replaceCount > 1 ? theme.fg("dim", ` (${replaceCount} replacements)`) : "";
|
|
356
|
+
/** 25 visual lines per hunk: head 12 + tail 13 */
|
|
357
|
+
const HUNK_EXCERPTS = [{
|
|
358
|
+
focus: "head",
|
|
359
|
+
context: 12
|
|
360
|
+
}, {
|
|
361
|
+
focus: "tail",
|
|
362
|
+
context: 13
|
|
363
|
+
}];
|
|
364
|
+
return {
|
|
365
|
+
render(width) {
|
|
366
|
+
const lines = [];
|
|
367
|
+
lines.push(statsText + replaceNote);
|
|
368
|
+
const displaySections = sections.map((s) => {
|
|
369
|
+
const blocks = !expanded && s.blocks.length > 1 ? s.blocks.slice(-1) : s.blocks;
|
|
370
|
+
const header = filePath ? osc8Link(`file://${filePath}`, s.header ?? "") : s.header;
|
|
371
|
+
return {
|
|
372
|
+
...s,
|
|
373
|
+
header,
|
|
374
|
+
blocks
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
const notices = [];
|
|
378
|
+
if (replaceCount && replaceCount > 1) notices.push(`replaced ${replaceCount} occurrences`);
|
|
379
|
+
const boxOutput = formatBoxesWindowed(displaySections, {
|
|
380
|
+
maxSections: expanded ? void 0 : 1,
|
|
381
|
+
excerpts: HUNK_EXCERPTS
|
|
382
|
+
}, notices.length > 0 ? notices : void 0, width);
|
|
383
|
+
lines.push(...boxOutput.split("\n"));
|
|
384
|
+
return lines;
|
|
385
|
+
},
|
|
386
|
+
invalidate() {}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function edit_file_default(pi) {
|
|
392
|
+
pi.registerTool(withPromptPatch(createEditFileTool()));
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
export { createEditFileTool, edit_file_default as default };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
//#region packages/extensions/finder/index.d.ts
|
|
4
|
+
interface FinderConfig {
|
|
5
|
+
systemPrompt?: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
extensionTools?: string[];
|
|
8
|
+
builtinTools?: string[];
|
|
9
|
+
}
|
|
10
|
+
declare function createFinderTool(config?: FinderConfig): ToolDefinition;
|
|
11
|
+
declare function export_default(pi: ExtensionAPI): void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { FinderConfig, createFinderTool, export_default as default };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { withPromptPatch } from "../core/prompt-patch.js";
|
|
2
|
+
import { getExtensionConfig } from "../core/config.js";
|
|
3
|
+
import { piSpawn, resolvePrompt, zeroUsage } from "../core/pi-spawn.js";
|
|
4
|
+
import { getFinalOutput, renderAgentTree, subAgentResult } from "../core/sub-agent-render.js";
|
|
5
|
+
import { Container, Text } from "@mariozechner/pi-tui";
|
|
6
|
+
import { Type } from "@sinclair/typebox";
|
|
7
|
+
//#region packages/extensions/finder/index.ts
|
|
8
|
+
const CONFIG_DEFAULTS = {
|
|
9
|
+
model: "openrouter/google/gemini-3-flash-preview",
|
|
10
|
+
extensionTools: [
|
|
11
|
+
"read",
|
|
12
|
+
"grep",
|
|
13
|
+
"find",
|
|
14
|
+
"ls"
|
|
15
|
+
],
|
|
16
|
+
builtinTools: [
|
|
17
|
+
"read",
|
|
18
|
+
"grep",
|
|
19
|
+
"find",
|
|
20
|
+
"ls"
|
|
21
|
+
],
|
|
22
|
+
promptFile: "",
|
|
23
|
+
promptString: ""
|
|
24
|
+
};
|
|
25
|
+
function createFinderTool(config = {}) {
|
|
26
|
+
return {
|
|
27
|
+
name: "finder",
|
|
28
|
+
label: "Finder",
|
|
29
|
+
description: "Intelligently search your codebase: Use it for complex, multi-step search tasks where you need to find code based on functionality or concepts rather than exact matches. Anytime you want to chain multiple grep calls you should use this tool.\n\nWHEN TO USE THIS TOOL:\n- You must locate code by behavior or concept\n- You need to run multiple greps in sequence\n- You must correlate or look for connection between several areas of the codebase\n- You must filter broad terms by context\n- You need answers to questions like \"Where do we validate JWT headers?\"\n\nWHEN NOT TO USE THIS TOOL:\n- When you know the exact file path - use Read directly\n- When looking for specific symbols or exact strings - use Find or Grep\n- When you need to create, modify files, or run terminal commands\n\nUSAGE GUIDELINES:\n1. Always spawn multiple search agents in parallel to maximise speed.\n2. Formulate your query as a precise engineering request.\n3. Name concrete artifacts, patterns, or APIs to narrow scope.\n4. State explicit success criteria so the agent knows when to stop.\n5. Never issue vague or exploratory commands.",
|
|
30
|
+
parameters: Type.Object({ query: Type.String({ description: "The search query describing what to find. Be specific and include technical terms, file types, or expected code patterns." }) }),
|
|
31
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
32
|
+
const p = params;
|
|
33
|
+
let sessionId = "";
|
|
34
|
+
try {
|
|
35
|
+
sessionId = ctx.sessionManager?.getSessionId?.() ?? "";
|
|
36
|
+
} catch {}
|
|
37
|
+
const singleResult = {
|
|
38
|
+
agent: "finder",
|
|
39
|
+
task: p.query,
|
|
40
|
+
exitCode: -1,
|
|
41
|
+
messages: [],
|
|
42
|
+
usage: zeroUsage()
|
|
43
|
+
};
|
|
44
|
+
const result = await piSpawn({
|
|
45
|
+
cwd: ctx.cwd,
|
|
46
|
+
task: p.query,
|
|
47
|
+
model: config.model ?? CONFIG_DEFAULTS.model,
|
|
48
|
+
builtinTools: config.builtinTools ?? CONFIG_DEFAULTS.builtinTools,
|
|
49
|
+
extensionTools: config.extensionTools ?? CONFIG_DEFAULTS.extensionTools,
|
|
50
|
+
systemPromptBody: config.systemPrompt,
|
|
51
|
+
signal,
|
|
52
|
+
sessionId,
|
|
53
|
+
onUpdate: (partial) => {
|
|
54
|
+
singleResult.messages = partial.messages;
|
|
55
|
+
singleResult.usage = partial.usage;
|
|
56
|
+
singleResult.model = partial.model;
|
|
57
|
+
singleResult.stopReason = partial.stopReason;
|
|
58
|
+
singleResult.errorMessage = partial.errorMessage;
|
|
59
|
+
if (onUpdate) onUpdate({
|
|
60
|
+
content: [{
|
|
61
|
+
type: "text",
|
|
62
|
+
text: getFinalOutput(partial.messages) || "(searching...)"
|
|
63
|
+
}],
|
|
64
|
+
details: singleResult
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
singleResult.exitCode = result.exitCode;
|
|
69
|
+
singleResult.messages = result.messages;
|
|
70
|
+
singleResult.usage = result.usage;
|
|
71
|
+
singleResult.model = result.model;
|
|
72
|
+
singleResult.stopReason = result.stopReason;
|
|
73
|
+
singleResult.errorMessage = result.errorMessage;
|
|
74
|
+
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
75
|
+
const output = getFinalOutput(result.messages) || "(no output)";
|
|
76
|
+
if (isError) return subAgentResult(result.errorMessage || result.stderr || output, singleResult, true);
|
|
77
|
+
return subAgentResult(output, singleResult);
|
|
78
|
+
},
|
|
79
|
+
renderCall(args, theme) {
|
|
80
|
+
const preview = args.query ? args.query.length > 80 ? `${args.query.slice(0, 80)}...` : args.query : "...";
|
|
81
|
+
return new Text(theme.fg("toolTitle", theme.bold("finder ")) + theme.fg("dim", preview), 0, 0);
|
|
82
|
+
},
|
|
83
|
+
renderResult(result, { expanded }, theme) {
|
|
84
|
+
const details = result.details;
|
|
85
|
+
if (!details) {
|
|
86
|
+
const text = result.content[0];
|
|
87
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
88
|
+
}
|
|
89
|
+
const container = new Container();
|
|
90
|
+
renderAgentTree(details, container, expanded, theme, {
|
|
91
|
+
label: "finder",
|
|
92
|
+
header: "statusOnly"
|
|
93
|
+
});
|
|
94
|
+
return container;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function finder_default(pi) {
|
|
99
|
+
const cfg = getExtensionConfig("@bds_pi/finder", CONFIG_DEFAULTS);
|
|
100
|
+
pi.registerTool(withPromptPatch(createFinderTool({
|
|
101
|
+
systemPrompt: resolvePrompt(cfg.promptString, cfg.promptFile),
|
|
102
|
+
model: cfg.model,
|
|
103
|
+
extensionTools: cfg.extensionTools,
|
|
104
|
+
builtinTools: cfg.builtinTools
|
|
105
|
+
})));
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
export { createFinderTool, finder_default as default };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
//#region packages/extensions/format-file/index.d.ts
|
|
4
|
+
type FormatFileExtConfig = {
|
|
5
|
+
preferredFormatter: string;
|
|
6
|
+
formatterLookupTimeoutMs: number;
|
|
7
|
+
};
|
|
8
|
+
declare function createFormatFileTool(config?: FormatFileExtConfig): ToolDefinition;
|
|
9
|
+
declare function export_default(pi: ExtensionAPI): void;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { createFormatFileTool, export_default as default };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { withPromptPatch } from "../core/prompt-patch.js";
|
|
2
|
+
import { boxRendererWindowed, osc8Link, textSection } from "../core/box-format.js";
|
|
3
|
+
import { withFileLock } from "../core/mutex.js";
|
|
4
|
+
import { getExtensionConfig } from "../core/config.js";
|
|
5
|
+
import { resolveWithVariants } from "./read.js";
|
|
6
|
+
import { saveChange, simpleDiff } from "../core/file-tracker.js";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
11
|
+
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
//#region packages/extensions/format-file/index.ts
|
|
14
|
+
/**
|
|
15
|
+
* format_file tool — runs a code formatter on a file.
|
|
16
|
+
*
|
|
17
|
+
* tries formatters in order: prettier, biome. uses whichever is
|
|
18
|
+
* available on PATH (nix provides these). captures before/after
|
|
19
|
+
* diff and tracks the change for undo_edit.
|
|
20
|
+
*/
|
|
21
|
+
const COLLAPSED_EXCERPTS = [{
|
|
22
|
+
focus: "head",
|
|
23
|
+
context: 3
|
|
24
|
+
}, {
|
|
25
|
+
focus: "tail",
|
|
26
|
+
context: 5
|
|
27
|
+
}];
|
|
28
|
+
const CONFIG_DEFAULTS = {
|
|
29
|
+
preferredFormatter: "auto",
|
|
30
|
+
formatterLookupTimeoutMs: 3e3
|
|
31
|
+
};
|
|
32
|
+
const FORMATTERS = [{
|
|
33
|
+
name: "prettier",
|
|
34
|
+
args: (file) => [
|
|
35
|
+
"--write",
|
|
36
|
+
"--log-level",
|
|
37
|
+
"silent",
|
|
38
|
+
file
|
|
39
|
+
]
|
|
40
|
+
}, {
|
|
41
|
+
name: "biome",
|
|
42
|
+
args: (file) => [
|
|
43
|
+
"format",
|
|
44
|
+
"--write",
|
|
45
|
+
file
|
|
46
|
+
]
|
|
47
|
+
}];
|
|
48
|
+
function findFormatter(preferred, timeoutMs) {
|
|
49
|
+
const ordered = preferred !== "auto" ? [...FORMATTERS].sort((a, b) => a.name === preferred ? -1 : b.name === preferred ? 1 : 0) : FORMATTERS;
|
|
50
|
+
for (const fmt of ordered) if (spawnSync("which", [fmt.name], {
|
|
51
|
+
encoding: "utf-8",
|
|
52
|
+
timeout: timeoutMs
|
|
53
|
+
}).status === 0) return fmt;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function createFormatFileTool(config = CONFIG_DEFAULTS) {
|
|
57
|
+
return {
|
|
58
|
+
name: "format_file",
|
|
59
|
+
label: "Format File",
|
|
60
|
+
description: "Run a code formatter (prettier or biome) on a file.",
|
|
61
|
+
parameters: Type.Object({ path: Type.String({ description: "The absolute path to the file to format." }) }),
|
|
62
|
+
renderCall(args, theme) {
|
|
63
|
+
const filePath = args.path || "...";
|
|
64
|
+
const home = os.homedir();
|
|
65
|
+
const shortened = filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
|
|
66
|
+
const linked = filePath.startsWith("/") ? osc8Link(`file://${filePath}`, shortened) : shortened;
|
|
67
|
+
return new Text(theme.fg("toolTitle", theme.bold("Format ")) + theme.fg("dim", linked), 0, 0);
|
|
68
|
+
},
|
|
69
|
+
renderResult(result, { expanded }, _theme) {
|
|
70
|
+
const content = result.content?.[0];
|
|
71
|
+
if (!content || content.type !== "text") return new Text("(no output)", 0, 0);
|
|
72
|
+
return boxRendererWindowed(() => [textSection(void 0, content.text)], {
|
|
73
|
+
collapsed: { excerpts: COLLAPSED_EXCERPTS },
|
|
74
|
+
expanded: {}
|
|
75
|
+
}, void 0, expanded);
|
|
76
|
+
},
|
|
77
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
78
|
+
const resolved = resolveWithVariants(params.path, ctx.cwd);
|
|
79
|
+
if (!fs.existsSync(resolved)) return {
|
|
80
|
+
content: [{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: `file not found: ${resolved}`
|
|
83
|
+
}],
|
|
84
|
+
isError: true
|
|
85
|
+
};
|
|
86
|
+
const formatter = findFormatter(config.preferredFormatter, config.formatterLookupTimeoutMs);
|
|
87
|
+
if (!formatter) return {
|
|
88
|
+
content: [{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: "no formatter found. install prettier or biome."
|
|
91
|
+
}],
|
|
92
|
+
isError: true
|
|
93
|
+
};
|
|
94
|
+
return withFileLock(resolved, async () => {
|
|
95
|
+
const before = fs.readFileSync(resolved, "utf-8");
|
|
96
|
+
const result = spawnSync(formatter.name, formatter.args(resolved), {
|
|
97
|
+
encoding: "utf-8",
|
|
98
|
+
timeout: 3e4,
|
|
99
|
+
cwd: ctx.cwd
|
|
100
|
+
});
|
|
101
|
+
if (result.status !== 0) {
|
|
102
|
+
const err = result.stderr?.trim() || result.stdout?.trim() || `exit code ${result.status}`;
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: `${formatter.name} failed: ${err}`
|
|
107
|
+
}],
|
|
108
|
+
isError: true
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const after = fs.readFileSync(resolved, "utf-8");
|
|
112
|
+
if (before === after) return {
|
|
113
|
+
content: [{
|
|
114
|
+
type: "text",
|
|
115
|
+
text: `${path.basename(resolved)} is already formatted.`
|
|
116
|
+
}],
|
|
117
|
+
details: { header: resolved }
|
|
118
|
+
};
|
|
119
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
120
|
+
const diff = simpleDiff(resolved, before, after);
|
|
121
|
+
saveChange(sessionId, toolCallId, {
|
|
122
|
+
uri: `file://${resolved}`,
|
|
123
|
+
before,
|
|
124
|
+
after,
|
|
125
|
+
diff,
|
|
126
|
+
isNewFile: false,
|
|
127
|
+
timestamp: Date.now()
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
content: [{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: `formatted ${path.basename(resolved)} with ${formatter.name}.\n\n${diff}`
|
|
133
|
+
}],
|
|
134
|
+
details: { header: resolved }
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function format_file_default(pi) {
|
|
141
|
+
const cfg = getExtensionConfig("@bds_pi/format-file", CONFIG_DEFAULTS);
|
|
142
|
+
pi.registerTool(withPromptPatch(createFormatFileTool(cfg)));
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
export { createFormatFileTool, format_file_default as default };
|