@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,163 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { boxRendererWindowed, textSection } from "../core/box-format.js";
3
+ import { formatHeadTail } from "../core/output-buffer.js";
4
+ import { getExtensionConfig } from "../core/config.js";
5
+ import * as path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { Text } from "@mariozechner/pi-tui";
8
+ import { Type } from "@sinclair/typebox";
9
+ import { createInterface } from "node:readline";
10
+ //#region packages/extensions/glob/index.ts
11
+ /**
12
+ * glob tool — replaces pi's built-in find with enhanced file finding.
13
+ *
14
+ * differences from pi's built-in:
15
+ * - uses rg --files (not fd — one less dependency)
16
+ * - sorted by mtime (most recent first, via rg --sortr modified)
17
+ * - pagination via offset + limit
18
+ * - hidden files included by default (--hidden)
19
+ * - .git/.jj excluded
20
+ *
21
+ * shadows pi's built-in `find` tool via same-name registration.
22
+ */
23
+ const COLLAPSED_EXCERPTS = [{
24
+ focus: "head",
25
+ context: 3
26
+ }, {
27
+ focus: "tail",
28
+ context: 5
29
+ }];
30
+ const CONFIG_DEFAULTS = { defaultLimit: 500 };
31
+ function createGlobTool(config = CONFIG_DEFAULTS) {
32
+ return {
33
+ name: "find",
34
+ label: "Find Files",
35
+ description: "Fast file pattern matching tool that works with any codebase size.\n\nReturns matching file paths sorted by most recent modification time first.\n\n## Pattern syntax\n- `**/*.js` — All JavaScript files in any directory\n- `src/**/*.ts` — TypeScript files under src/\n- `*.json` — JSON files in the current directory\n- `**/*test*` — Files with \"test\" in their name\n- `**/*.{js,ts}` — JavaScript and TypeScript files\n",
36
+ parameters: Type.Object({
37
+ filePattern: Type.String({ description: "Glob pattern like \"**/*.js\" or \"src/**/*.ts\" to match files." }),
38
+ limit: Type.Optional(Type.Number({ description: "Maximum number of results to return." })),
39
+ offset: Type.Optional(Type.Number({ description: "Number of results to skip (for pagination)." }))
40
+ }),
41
+ renderCall(args, theme) {
42
+ const pattern = args.filePattern || "...";
43
+ return new Text(theme.fg("toolTitle", theme.bold("Find ")) + theme.fg("dim", pattern), 0, 0);
44
+ },
45
+ renderResult(result, { expanded }, _theme) {
46
+ const content = result.content?.[0];
47
+ if (!content || content.type !== "text") return new Text("(no output)", 0, 0);
48
+ return boxRendererWindowed(() => [textSection(void 0, content.text)], {
49
+ collapsed: { excerpts: COLLAPSED_EXCERPTS },
50
+ expanded: {}
51
+ }, void 0, expanded);
52
+ },
53
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
54
+ const p = params;
55
+ const searchPath = ctx.cwd;
56
+ const limit = p.limit ?? config.defaultLimit;
57
+ const offset = p.offset ?? 0;
58
+ return new Promise((resolve) => {
59
+ const child = spawn("rg", [
60
+ "--files",
61
+ "--hidden",
62
+ "--color=never",
63
+ "--sortr",
64
+ "modified",
65
+ "--glob",
66
+ "!.git",
67
+ "--glob",
68
+ "!.jj",
69
+ "--glob",
70
+ p.filePattern,
71
+ searchPath
72
+ ], { stdio: [
73
+ "ignore",
74
+ "pipe",
75
+ "pipe"
76
+ ] });
77
+ const rl = createInterface({ input: child.stdout });
78
+ let stderr = "";
79
+ let aborted = false;
80
+ const allPaths = [];
81
+ const onAbort = () => {
82
+ aborted = true;
83
+ if (!child.killed) child.kill();
84
+ };
85
+ signal?.addEventListener("abort", onAbort, { once: true });
86
+ child.stderr?.on("data", (chunk) => {
87
+ stderr += chunk.toString();
88
+ });
89
+ rl.on("line", (line) => {
90
+ const trimmed = line.trim();
91
+ if (!trimmed) return;
92
+ const rel = path.relative(searchPath, trimmed).replace(/\\/g, "/");
93
+ if (rel && !rel.startsWith("..")) allPaths.push(rel);
94
+ });
95
+ child.on("error", (err) => {
96
+ rl.close();
97
+ signal?.removeEventListener("abort", onAbort);
98
+ resolve({
99
+ content: [{
100
+ type: "text",
101
+ text: `find error: ${err.message}`
102
+ }],
103
+ isError: true
104
+ });
105
+ });
106
+ child.on("close", (code) => {
107
+ rl.close();
108
+ signal?.removeEventListener("abort", onAbort);
109
+ if (aborted) {
110
+ resolve({
111
+ content: [{
112
+ type: "text",
113
+ text: "search aborted"
114
+ }],
115
+ isError: true
116
+ });
117
+ return;
118
+ }
119
+ if (code !== 0 && code !== 1 && allPaths.length === 0) {
120
+ resolve({
121
+ content: [{
122
+ type: "text",
123
+ text: stderr.trim() || `rg exited with code ${code}`
124
+ }],
125
+ isError: true
126
+ });
127
+ return;
128
+ }
129
+ if (allPaths.length === 0) {
130
+ resolve({ content: [{
131
+ type: "text",
132
+ text: "no files found matching pattern"
133
+ }] });
134
+ return;
135
+ }
136
+ const total = allPaths.length;
137
+ let output;
138
+ if (offset > 0) {
139
+ const paginated = allPaths.slice(offset, offset + limit);
140
+ output = paginated.join("\n");
141
+ output += `\n\n(showing ${offset + 1}-${offset + paginated.length} of ${total} results)`;
142
+ } else if (total > limit) {
143
+ output = formatHeadTail(allPaths, limit, (n) => `... [${n} more results, use a more specific pattern to narrow] ...`);
144
+ output += `\n\n(${total} total results)`;
145
+ } else output = allPaths.join("\n");
146
+ resolve({
147
+ content: [{
148
+ type: "text",
149
+ text: output
150
+ }],
151
+ details: { header: p.filePattern }
152
+ });
153
+ });
154
+ });
155
+ }
156
+ };
157
+ }
158
+ function glob_default(pi) {
159
+ const cfg = getExtensionConfig("@bds_pi/glob", CONFIG_DEFAULTS);
160
+ pi.registerTool(withPromptPatch(createGlobTool(cfg)));
161
+ }
162
+ //#endregion
163
+ export { createGlobTool, glob_default as default };
@@ -0,0 +1,13 @@
1
+ import { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/grep/index.d.ts
4
+ type GrepExtConfig = {
5
+ maxTotalMatches: number;
6
+ maxPerFile: number;
7
+ maxLineChars: number;
8
+ contextLines: number;
9
+ };
10
+ declare function createGrepTool(config?: GrepExtConfig): ToolDefinition;
11
+ declare function export_default(pi: ExtensionAPI): void;
12
+ //#endregion
13
+ export { createGrepTool, export_default as default };
@@ -0,0 +1,356 @@
1
+ import { withPromptPatch } from "../core/prompt-patch.js";
2
+ import { boxRendererWindowed, osc8Link } from "../core/box-format.js";
3
+ import { headTail } from "../core/output-buffer.js";
4
+ import { getExtensionConfig } from "../core/config.js";
5
+ import * as path from "node:path";
6
+ import { spawn } from "node:child_process";
7
+ import { Text } from "@mariozechner/pi-tui";
8
+ import { Type } from "@sinclair/typebox";
9
+ import * as os from "node:os";
10
+ import { createInterface } from "node:readline";
11
+ //#region packages/extensions/grep/index.ts
12
+ /**
13
+ * grep tool — replaces pi's built-in with tighter limits and context lines.
14
+ *
15
+ * differences from pi's built-in:
16
+ * - per-file match limit (10, prevents one noisy file from consuming quota)
17
+ * - 200-char line truncation (vs pi's 500)
18
+ * - caseSensitive param (default case-sensitive)
19
+ * - suggests literal:true when pattern contains regex metacharacters
20
+ * - spawns rg directly (no ensureTool — nix provides rg on PATH)
21
+ * - includes ±1 context lines around matches (via rg --context) so
22
+ * the LLM sees surrounding code and show() has natural gaps to elide
23
+ *
24
+ * shadows pi's built-in `grep` tool via same-name registration.
25
+ */
26
+ const CONFIG_DEFAULTS = {
27
+ maxTotalMatches: 100,
28
+ maxPerFile: 10,
29
+ maxLineChars: 200,
30
+ contextLines: 1
31
+ };
32
+ /** max files shown in collapsed display */
33
+ const COLLAPSED_MAX_FILES = 3;
34
+ /** per-block excerpts for collapsed display — show first 5 visual lines */
35
+ const COLLAPSED_EXCERPTS = [{
36
+ focus: "head",
37
+ context: 5
38
+ }];
39
+ function truncateLine(line, maxChars) {
40
+ if (line.length <= maxChars) return line;
41
+ return line.slice(0, maxChars) + "...";
42
+ }
43
+ function looksLikeRegex(pattern) {
44
+ return /[{}()[\]|\\+*?^$]/.test(pattern);
45
+ }
46
+ /**
47
+ * convert GrepFile[] to BoxSection[] for box-format rendering.
48
+ * each file becomes a section, contiguous match groups become blocks.
49
+ */
50
+ function grepToSections(files, maxPerFile) {
51
+ return files.map((f) => {
52
+ const blocks = [];
53
+ let current = [];
54
+ let lastLineNum = -2;
55
+ for (const m of f.matches) {
56
+ if (lastLineNum >= 0 && m.lineNum > lastLineNum + 1) {
57
+ if (current.length > 0) {
58
+ blocks.push({ lines: current });
59
+ current = [];
60
+ }
61
+ }
62
+ current.push({
63
+ gutter: String(m.lineNum),
64
+ text: m.text,
65
+ highlight: !m.isContext
66
+ });
67
+ lastLineNum = m.lineNum;
68
+ }
69
+ if (current.length > 0) blocks.push({ lines: current });
70
+ return {
71
+ header: f.path + (f.hitLimit ? ` [${maxPerFile} match limit]` : ""),
72
+ blocks
73
+ };
74
+ });
75
+ }
76
+ function createGrepTool(config = CONFIG_DEFAULTS) {
77
+ const maxCollectMatches = config.maxTotalMatches * 2;
78
+ return {
79
+ name: "grep",
80
+ label: "Grep",
81
+ description: `Search for exact text patterns in files using ripgrep, a fast keyword search tool.
82
+
83
+ # When to use
84
+ - Finding exact text matches (variable names, function calls, specific strings)
85
+
86
+ # Constraints
87
+ - Results are limited to ${config.maxTotalMatches} matches (up to ${config.maxPerFile} per file)\n- Lines are truncated at ${config.maxLineChars} characters\n\n# Strategy
88
+ - Use 'path' or 'glob' to narrow searches; run multiple focused calls rather than one broad search
89
+ - Uses Rust-style regex (escape \`{\` and \`}\`); use \`literal: true\` for literal text search
90
+ `,
91
+ parameters: Type.Object({
92
+ pattern: Type.String({ description: "The pattern to search for (regex by default)." }),
93
+ path: Type.Optional(Type.String({ description: "The file or directory path to search in. Cannot be used with glob." })),
94
+ glob: Type.Optional(Type.String({ description: "The glob pattern to filter files (e.g., '**/*.ts'). Cannot be used with path." })),
95
+ caseSensitive: Type.Optional(Type.Boolean({ description: "Whether to search case-sensitively (default: true)." })),
96
+ literal: Type.Optional(Type.Boolean({ description: "Whether to treat the pattern as a literal string instead of a regex." }))
97
+ }),
98
+ renderCall(args, theme) {
99
+ const pattern = args.pattern || "...";
100
+ const searchPath = args.path || args.glob || ".";
101
+ const home = os.homedir();
102
+ const shortened = searchPath.startsWith(home) ? `~${searchPath.slice(home.length)}` : searchPath;
103
+ const linkedPath = searchPath.startsWith("/") ? osc8Link(`file://${searchPath}`, shortened) : shortened;
104
+ const caseSuffix = args.caseSensitive === false ? " -i" : "";
105
+ return new Text(theme.fg("toolTitle", theme.bold("Grep ")) + theme.fg("dim", `/${pattern}/${caseSuffix} in ${linkedPath}`), 0, 0);
106
+ },
107
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
108
+ const p = params;
109
+ const searchPath = p.path ? path.isAbsolute(p.path) ? p.path : path.resolve(ctx.cwd, p.path) : ctx.cwd;
110
+ return new Promise((resolve) => {
111
+ const args = [
112
+ "--json",
113
+ "--line-number",
114
+ "--color=never",
115
+ "--hidden",
116
+ "--context",
117
+ String(config.contextLines)
118
+ ];
119
+ if (p.caseSensitive === false) args.push("--ignore-case");
120
+ if (p.literal) args.push("--fixed-strings");
121
+ if (p.glob) args.push("--glob", p.glob);
122
+ args.push("--", p.pattern, searchPath);
123
+ const child = spawn("rg", args, { stdio: [
124
+ "ignore",
125
+ "pipe",
126
+ "pipe"
127
+ ] });
128
+ const rl = createInterface({ input: child.stdout });
129
+ let stderr = "";
130
+ let totalMatches = 0;
131
+ let killedDueToLimit = false;
132
+ let aborted = false;
133
+ /** all match + context events, in stream order */
134
+ const events = [];
135
+ const onAbort = () => {
136
+ aborted = true;
137
+ if (!child.killed) child.kill();
138
+ };
139
+ signal?.addEventListener("abort", onAbort, { once: true });
140
+ child.stderr?.on("data", (chunk) => {
141
+ stderr += chunk.toString();
142
+ });
143
+ rl.on("line", (line) => {
144
+ if (!line.trim() || killedDueToLimit) return;
145
+ let event;
146
+ try {
147
+ event = JSON.parse(line);
148
+ } catch {
149
+ return;
150
+ }
151
+ if (event.type !== "match" && event.type !== "context") return;
152
+ const filePath = event.data?.path?.text;
153
+ const lineNumber = event.data?.line_number;
154
+ const lineText = (event.data?.lines?.text ?? "").replace(/\r?\n$/, "");
155
+ if (!filePath || typeof lineNumber !== "number") return;
156
+ if (event.type === "match") totalMatches++;
157
+ events.push({
158
+ kind: event.type,
159
+ filePath,
160
+ lineNumber,
161
+ lineText
162
+ });
163
+ if (totalMatches >= maxCollectMatches) {
164
+ killedDueToLimit = true;
165
+ if (!child.killed) child.kill();
166
+ }
167
+ });
168
+ child.on("error", (err) => {
169
+ rl.close();
170
+ signal?.removeEventListener("abort", onAbort);
171
+ resolve({
172
+ content: [{
173
+ type: "text",
174
+ text: `grep error: ${err.message}`
175
+ }],
176
+ isError: true
177
+ });
178
+ });
179
+ child.on("close", (code) => {
180
+ rl.close();
181
+ signal?.removeEventListener("abort", onAbort);
182
+ if (aborted) {
183
+ resolve({
184
+ content: [{
185
+ type: "text",
186
+ text: "search aborted"
187
+ }],
188
+ isError: true
189
+ });
190
+ return;
191
+ }
192
+ if (!killedDueToLimit && code !== 0 && code !== 1) {
193
+ resolve({
194
+ content: [{
195
+ type: "text",
196
+ text: stderr.trim() || `ripgrep exited with code ${code}`
197
+ }],
198
+ isError: true
199
+ });
200
+ return;
201
+ }
202
+ if (totalMatches === 0) {
203
+ let text = "no matches found";
204
+ if (!p.literal && looksLikeRegex(p.pattern)) text += "\n\n(pattern contains regex characters — try literal: true if searching for exact text)";
205
+ resolve({ content: [{
206
+ type: "text",
207
+ text
208
+ }] });
209
+ return;
210
+ }
211
+ const fileOrder = [];
212
+ const fileEvents = /* @__PURE__ */ new Map();
213
+ for (const ev of events) {
214
+ if (!fileEvents.has(ev.filePath)) {
215
+ fileOrder.push(ev.filePath);
216
+ fileEvents.set(ev.filePath, []);
217
+ }
218
+ fileEvents.get(ev.filePath).push(ev);
219
+ }
220
+ const outputLines = [];
221
+ /** output line indices that are actual matches (not context) */
222
+ const matchLineIndices = [];
223
+ /** index of first match line per file — focus points for collapsed display */
224
+ const firstMatchPerFile = [];
225
+ const perFileMatchCount = /* @__PURE__ */ new Map();
226
+ const fileGroups = [];
227
+ for (let fi = 0; fi < fileOrder.length; fi++) {
228
+ const filePath = fileOrder[fi];
229
+ if (!filePath) continue;
230
+ const fileEvts = fileEvents.get(filePath);
231
+ const includedMatchLines = /* @__PURE__ */ new Set();
232
+ let matchesInFile = 0;
233
+ for (const ev of fileEvts) if (ev.kind === "match") {
234
+ matchesInFile++;
235
+ if (matchesInFile <= config.maxPerFile) includedMatchLines.add(ev.lineNumber);
236
+ }
237
+ perFileMatchCount.set(filePath, Math.min(matchesInFile, config.maxPerFile));
238
+ const includedLines = /* @__PURE__ */ new Set();
239
+ for (const ln of includedMatchLines) {
240
+ includedLines.add(ln);
241
+ for (let d = 1; d <= config.contextLines; d++) {
242
+ includedLines.add(ln - d);
243
+ includedLines.add(ln + d);
244
+ }
245
+ }
246
+ if (fi > 0) outputLines.push("");
247
+ const rel = path.relative(searchPath, filePath).replace(/\\/g, "/");
248
+ const displayPath = rel && !rel.startsWith("..") ? rel : path.basename(filePath);
249
+ const grepFile = {
250
+ path: displayPath,
251
+ matches: [],
252
+ hitLimit: matchesInFile > config.maxPerFile
253
+ };
254
+ let lastOutputLineNum = -Infinity;
255
+ let isFirstMatchInFile = true;
256
+ for (const ev of fileEvts) {
257
+ if (!includedLines.has(ev.lineNumber)) continue;
258
+ if (ev.lineNumber <= lastOutputLineNum) continue;
259
+ if (lastOutputLineNum >= 0 && ev.lineNumber > lastOutputLineNum + 1) outputLines.push("--");
260
+ const idx = outputLines.length;
261
+ outputLines.push(`${displayPath}:${ev.lineNumber}: ${truncateLine(ev.lineText, config.maxLineChars)}`);
262
+ lastOutputLineNum = ev.lineNumber;
263
+ grepFile.matches.push({
264
+ lineNum: ev.lineNumber,
265
+ text: truncateLine(ev.lineText, config.maxLineChars),
266
+ isContext: ev.kind === "context"
267
+ });
268
+ if (includedMatchLines.has(ev.lineNumber)) {
269
+ matchLineIndices.push(idx);
270
+ if (isFirstMatchInFile) {
271
+ firstMatchPerFile.push(idx);
272
+ isFirstMatchInFile = false;
273
+ }
274
+ }
275
+ }
276
+ fileGroups.push(grepFile);
277
+ }
278
+ let output;
279
+ const notices = [];
280
+ let finalMatchIndices;
281
+ if (outputLines.length > config.maxTotalMatches * 3) {
282
+ const limit = config.maxTotalMatches * 2;
283
+ const { head, tail, truncatedCount } = headTail(outputLines, limit);
284
+ output = [
285
+ ...head,
286
+ "",
287
+ `... [${truncatedCount} lines truncated] ...`,
288
+ "",
289
+ ...tail
290
+ ].join("\n");
291
+ const headLen = head.length;
292
+ const gapLen = 3;
293
+ const tailStart = outputLines.length - tail.length;
294
+ finalMatchIndices = matchLineIndices.map((i) => {
295
+ if (i < headLen) return i;
296
+ if (i >= tailStart) return headLen + gapLen + (i - tailStart);
297
+ return -1;
298
+ }).filter((i) => i >= 0);
299
+ notices.push(`${truncatedCount} lines truncated, showing first and last ${limit / 2}`);
300
+ } else {
301
+ output = outputLines.join("\n");
302
+ finalMatchIndices = matchLineIndices;
303
+ }
304
+ if (killedDueToLimit) notices.push(`stopped at ${maxCollectMatches} matches — refine pattern`);
305
+ const filesAtLimit = Array.from(perFileMatchCount.values()).filter((c) => c >= config.maxPerFile).length;
306
+ if (filesAtLimit > 0) notices.push(`${filesAtLimit} file${filesAtLimit > 1 ? "s" : ""} hit the ${config.maxPerFile}-per-file limit`);
307
+ if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
308
+ resolve({
309
+ content: [{
310
+ type: "text",
311
+ text: output
312
+ }],
313
+ details: {
314
+ fileGroups,
315
+ notices,
316
+ matchLineIndices: finalMatchIndices,
317
+ firstMatchPerFile,
318
+ searchPath
319
+ }
320
+ });
321
+ });
322
+ });
323
+ },
324
+ renderResult(result, { expanded }, _theme) {
325
+ const fileGroups = result.details?.fileGroups;
326
+ const notices = result.details?.notices ?? [];
327
+ const basePath = result.details?.searchPath;
328
+ if (!fileGroups?.length) return new Text(result.content?.[0]?.text ?? "(no output)", 0, 0);
329
+ const sections = grepToSections(fileGroups, config.maxPerFile);
330
+ if (basePath) for (let i = 0; i < sections.length; i++) {
331
+ const fg = fileGroups[i];
332
+ const section = sections[i];
333
+ if (!fg || !section) continue;
334
+ const relPath = fg.path;
335
+ const absPath = basePath + "/" + relPath;
336
+ sections[i] = {
337
+ ...section,
338
+ header: osc8Link(`file://${absPath}`, section.header ?? "")
339
+ };
340
+ }
341
+ return boxRendererWindowed(() => sections, {
342
+ collapsed: {
343
+ maxSections: COLLAPSED_MAX_FILES,
344
+ excerpts: COLLAPSED_EXCERPTS
345
+ },
346
+ expanded: {}
347
+ }, notices.length > 0 ? notices : void 0, expanded);
348
+ }
349
+ };
350
+ }
351
+ function grep_default(pi) {
352
+ const cfg = getExtensionConfig("@bds_pi/grep", CONFIG_DEFAULTS);
353
+ pi.registerTool(withPromptPatch(createGrepTool(cfg)));
354
+ }
355
+ //#endregion
356
+ export { createGrepTool, grep_default as default };
@@ -0,0 +1,6 @@
1
+ import { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+
3
+ //#region packages/extensions/handoff/index.d.ts
4
+ declare function export_default(pi: ExtensionAPI): void;
5
+ //#endregion
6
+ export { export_default as default };