@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.
Files changed (99) hide show
  1. package/dist/chunk-DsIazq42.js +26 -0
  2. package/dist/command-palette-B97yKqne.js +406 -0
  3. package/dist/core/agents-md.d.ts +22 -0
  4. package/dist/core/agents-md.js +174 -0
  5. package/dist/core/box-chrome.d.ts +56 -0
  6. package/dist/core/box-chrome.js +207 -0
  7. package/dist/core/box-format.d.ts +82 -0
  8. package/dist/core/box-format.js +381 -0
  9. package/dist/core/config.d.ts +14 -0
  10. package/dist/core/config.js +201 -0
  11. package/dist/core/file-tracker.d.ts +65 -0
  12. package/dist/core/file-tracker.js +439 -0
  13. package/dist/core/github-api.d.ts +45 -0
  14. package/dist/core/github-api.js +101 -0
  15. package/dist/core/html-to-md.d.ts +5 -0
  16. package/dist/core/html-to-md.js +289 -0
  17. package/dist/core/interpolate.d.ts +44 -0
  18. package/dist/core/interpolate.js +475 -0
  19. package/dist/core/mutex.d.ts +8 -0
  20. package/dist/core/mutex.js +142 -0
  21. package/dist/core/output-buffer.d.ts +80 -0
  22. package/dist/core/output-buffer.js +517 -0
  23. package/dist/core/permissions.d.ts +19 -0
  24. package/dist/core/permissions.js +140 -0
  25. package/dist/core/pi-spawn.d.ts +62 -0
  26. package/dist/core/pi-spawn.js +244 -0
  27. package/dist/core/prompt-patch.d.ts +11 -0
  28. package/dist/core/prompt-patch.js +16 -0
  29. package/dist/core/show-renderer.d.ts +10 -0
  30. package/dist/core/show-renderer.js +39 -0
  31. package/dist/core/show.d.ts +29 -0
  32. package/dist/core/show.js +288 -0
  33. package/dist/core/sub-agent-render.d.ts +56 -0
  34. package/dist/core/sub-agent-render.js +579 -0
  35. package/dist/core/tool-cost.d.ts +17 -0
  36. package/dist/core/tool-cost.js +58 -0
  37. package/dist/core/tui.d.ts +9 -0
  38. package/dist/core/tui.js +33 -0
  39. package/dist/editor-BljciPdx.js +537 -0
  40. package/dist/extensions/bash.d.ts +12 -0
  41. package/dist/extensions/bash.js +261 -0
  42. package/dist/extensions/code-review.d.ts +14 -0
  43. package/dist/extensions/code-review.js +175 -0
  44. package/dist/extensions/command-palette.d.ts +6 -0
  45. package/dist/extensions/command-palette.js +2 -0
  46. package/dist/extensions/create-file.d.ts +7 -0
  47. package/dist/extensions/create-file.js +88 -0
  48. package/dist/extensions/edit-file.d.ts +7 -0
  49. package/dist/extensions/edit-file.js +395 -0
  50. package/dist/extensions/editor.d.ts +6 -0
  51. package/dist/extensions/editor.js +2 -0
  52. package/dist/extensions/finder.d.ts +13 -0
  53. package/dist/extensions/finder.js +108 -0
  54. package/dist/extensions/format-file.d.ts +11 -0
  55. package/dist/extensions/format-file.js +145 -0
  56. package/dist/extensions/github.d.ts +13 -0
  57. package/dist/extensions/github.js +527 -0
  58. package/dist/extensions/glob.d.ts +10 -0
  59. package/dist/extensions/glob.js +163 -0
  60. package/dist/extensions/grep.d.ts +13 -0
  61. package/dist/extensions/grep.js +356 -0
  62. package/dist/extensions/handoff.d.ts +6 -0
  63. package/dist/extensions/handoff.js +277 -0
  64. package/dist/extensions/librarian.d.ts +13 -0
  65. package/dist/extensions/librarian.js +113 -0
  66. package/dist/extensions/look-at.d.ts +13 -0
  67. package/dist/extensions/look-at.js +121 -0
  68. package/dist/extensions/ls.d.ts +8 -0
  69. package/dist/extensions/ls.js +87 -0
  70. package/dist/extensions/mermaid.d.ts +20 -0
  71. package/dist/extensions/mermaid.js +2 -0
  72. package/dist/extensions/oracle.d.ts +13 -0
  73. package/dist/extensions/oracle.js +143 -0
  74. package/dist/extensions/read-session.d.ts +13 -0
  75. package/dist/extensions/read-session.js +265 -0
  76. package/dist/extensions/read-web-page.d.ts +11 -0
  77. package/dist/extensions/read-web-page.js +234 -0
  78. package/dist/extensions/read.d.ts +23 -0
  79. package/dist/extensions/read.js +323 -0
  80. package/dist/extensions/search-sessions.d.ts +29 -0
  81. package/dist/extensions/search-sessions.js +426 -0
  82. package/dist/extensions/session-name.d.ts +6 -0
  83. package/dist/extensions/session-name.js +54 -0
  84. package/dist/extensions/skill.d.ts +7 -0
  85. package/dist/extensions/skill.js +232 -0
  86. package/dist/extensions/system-prompt.d.ts +6 -0
  87. package/dist/extensions/system-prompt.js +31 -0
  88. package/dist/extensions/task.d.ts +11 -0
  89. package/dist/extensions/task.js +114 -0
  90. package/dist/extensions/tool-harness.d.ts +6 -0
  91. package/dist/extensions/tool-harness.js +37 -0
  92. package/dist/extensions/undo-edit.d.ts +7 -0
  93. package/dist/extensions/undo-edit.js +127 -0
  94. package/dist/extensions/web-search.d.ts +12 -0
  95. package/dist/extensions/web-search.js +254 -0
  96. package/dist/extensions.d.ts +29 -0
  97. package/dist/extensions.js +29 -0
  98. package/dist/mermaid-C7xCVBrw.js +95296 -0
  99. 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,6 @@
1
+ import { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/editor/index.d.ts
4
+ declare function export_default(pi: ExtensionAPI): void;
5
+ //#endregion
6
+ export { export_default as default };
@@ -0,0 +1,2 @@
1
+ import { t as editor_default } from "../editor-BljciPdx.js";
2
+ export { editor_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 };