@trim21/personal-pi-extensions 0.0.230 → 0.0.232
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/package.json +1 -1
- package/src/claude-code/grep.ts +74 -3
- package/src/spawn-agent.ts +17 -5
package/package.json
CHANGED
package/src/claude-code/grep.ts
CHANGED
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Split out of the former search.ts so spawn-agent subagents can load it
|
|
5
5
|
* independently (declare `Grep` in the frontmatter to get only this tool).
|
|
6
|
+
*
|
|
7
|
+
* Parameter semantics and default behavior are aligned with Claude Code's
|
|
8
|
+
* GrepTool (ripgrep-based): hidden files searched, VCS directories excluded,
|
|
9
|
+
* lines capped at 500 columns, and an implicit head_limit of 250 entries.
|
|
6
10
|
*/
|
|
7
11
|
|
|
12
|
+
import { stat } from "node:fs/promises";
|
|
13
|
+
|
|
8
14
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
9
15
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
16
|
import { Type } from "typebox";
|
|
@@ -15,6 +21,16 @@ const GREP_OUTPUT_MODES = ["content", "files_with_matches", "count"] as const;
|
|
|
15
21
|
|
|
16
22
|
type GrepOutputMode = (typeof GREP_OUTPUT_MODES)[number];
|
|
17
23
|
|
|
24
|
+
/** Version control directories excluded from searches (noise in results). */
|
|
25
|
+
const VCS_DIRECTORIES_TO_EXCLUDE = [".git", ".svn", ".hg", ".bzr", ".jj", ".sl"] as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Default cap on results when head_limit is unspecified. Prevents broad
|
|
29
|
+
* patterns from flooding the context; pass head_limit=0 explicitly for
|
|
30
|
+
* unlimited results. Mirrors Claude Code's default of 250.
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_HEAD_LIMIT = 250;
|
|
33
|
+
|
|
18
34
|
function truncateOutput(output: string, maxCharacters = 30_000): string {
|
|
19
35
|
if (output.length <= maxCharacters) return output;
|
|
20
36
|
return `${output.slice(0, maxCharacters)}\n\n[Output truncated at ${maxCharacters} characters]`;
|
|
@@ -39,7 +55,10 @@ interface GrepParameters {
|
|
|
39
55
|
|
|
40
56
|
export function buildGrepArguments(params: GrepParameters, cwd: string): string[] {
|
|
41
57
|
const mode = params.output_mode ?? "files_with_matches";
|
|
42
|
-
const args = ["--color=never"];
|
|
58
|
+
const args = ["--color=never", "--hidden", "--max-columns", "500"];
|
|
59
|
+
for (const dir of VCS_DIRECTORIES_TO_EXCLUDE) {
|
|
60
|
+
args.push("--glob", `!${dir}`);
|
|
61
|
+
}
|
|
43
62
|
switch (mode) {
|
|
44
63
|
case "files_with_matches": {
|
|
45
64
|
args.push("--files-with-matches");
|
|
@@ -51,7 +70,8 @@ export function buildGrepArguments(params: GrepParameters, cwd: string): string[
|
|
|
51
70
|
}
|
|
52
71
|
case "content": {
|
|
53
72
|
args.push("--no-heading", "--with-filename");
|
|
54
|
-
|
|
73
|
+
const showLineNumbers = params["-n"] === true || params["-n"] === undefined;
|
|
74
|
+
if (showLineNumbers) args.push("--line-number");
|
|
55
75
|
const before = params["-B"];
|
|
56
76
|
const after = params["-A"];
|
|
57
77
|
const around = params.context ?? params["-C"];
|
|
@@ -72,6 +92,43 @@ export function buildGrepArguments(params: GrepParameters, cwd: string): string[
|
|
|
72
92
|
return args;
|
|
73
93
|
}
|
|
74
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Sort a newline-separated list of file paths by modification time, most
|
|
97
|
+
* recent first, with file name as a tiebreaker. Files that can no longer be
|
|
98
|
+
* stat-ed sort last (mtime 0). Used for files_with_matches output, matching
|
|
99
|
+
* Claude Code's behaviour.
|
|
100
|
+
*/
|
|
101
|
+
export async function sortFilesByMtime(output: string): Promise<string> {
|
|
102
|
+
const paths = output.replace(/\n$/, "").split("\n").filter(Boolean);
|
|
103
|
+
if (paths.length <= 1) return output;
|
|
104
|
+
const stats = await Promise.allSettled(paths.map((path) => stat(path)));
|
|
105
|
+
const sorted = paths
|
|
106
|
+
.map((path, index) => ({
|
|
107
|
+
path,
|
|
108
|
+
mtimeMs: stats[index]?.status === "fulfilled" ? stats[index].value.mtimeMs : 0,
|
|
109
|
+
}))
|
|
110
|
+
.toSorted((left, right) => right.mtimeMs - left.mtimeMs || left.path.localeCompare(right.path))
|
|
111
|
+
.map((entry) => entry.path);
|
|
112
|
+
return sorted.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Append an occurrence/file summary to `filename:count` output (count mode),
|
|
117
|
+
* mirroring Claude Code's "Found N occurrences across M files" result.
|
|
118
|
+
*/
|
|
119
|
+
export function summarizeCountOutput(output: string): string {
|
|
120
|
+
const lines = output.split("\n").filter((line) => line.includes(":"));
|
|
121
|
+
let occurrences = 0;
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const colon = line.lastIndexOf(":");
|
|
124
|
+
occurrences += Number(line.slice(colon + 1)) || 0;
|
|
125
|
+
}
|
|
126
|
+
const files = lines.length;
|
|
127
|
+
const occurrenceLabel = occurrences === 1 ? "occurrence" : "occurrences";
|
|
128
|
+
const fileLabel = files === 1 ? "file" : "files";
|
|
129
|
+
return `${output.trimEnd()}\n\nFound ${occurrences} total ${occurrenceLabel} across ${files} ${fileLabel}.`;
|
|
130
|
+
}
|
|
131
|
+
|
|
75
132
|
export function pageGrepOutput(output: string, offset = 0, headLimit = 0): string {
|
|
76
133
|
const lines = output ? output.replace(/\n$/, "").split("\n") : [];
|
|
77
134
|
if (offset >= lines.length && lines.length > 0) return "No entries at this offset";
|
|
@@ -154,7 +211,21 @@ export function registerGrepTool(pi: ExtensionAPI): void {
|
|
|
154
211
|
if (result.code === 1 || result.stdout === "") {
|
|
155
212
|
return { content: [{ type: "text", text: "No files found" }], details: { matches: 0 } };
|
|
156
213
|
}
|
|
157
|
-
const
|
|
214
|
+
const mode = params.output_mode ?? "files_with_matches";
|
|
215
|
+
// head_limit defaults to DEFAULT_HEAD_LIMIT; an explicit 0 means unlimited.
|
|
216
|
+
const stdout =
|
|
217
|
+
mode === "files_with_matches" ? await sortFilesByMtime(result.stdout) : result.stdout;
|
|
218
|
+
const text = pageGrepOutput(
|
|
219
|
+
stdout,
|
|
220
|
+
params.offset ?? 0,
|
|
221
|
+
params.head_limit ?? DEFAULT_HEAD_LIMIT,
|
|
222
|
+
);
|
|
223
|
+
if (mode === "count") {
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: "text", text: summarizeCountOutput(text) }],
|
|
226
|
+
details: undefined,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
158
229
|
return { content: [{ type: "text", text }], details: undefined };
|
|
159
230
|
},
|
|
160
231
|
});
|
package/src/spawn-agent.ts
CHANGED
|
@@ -60,9 +60,11 @@ const MAX_PROGRESS_LINES = 5;
|
|
|
60
60
|
* without bash need no bwrap setup (there are no commands to sandbox).
|
|
61
61
|
* (Workspace write protection is embedded in the opencode write/edit tools.)
|
|
62
62
|
*
|
|
63
|
-
* Claude Code style tools (capitalized names
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* Claude Code style tools (capitalized names) map to their claude-code
|
|
64
|
+
* files, so a subagent can enable exactly the tools it declares — e.g. `Grep`
|
|
65
|
+
* without `Glob`. The stateful file tools (`Read`/`Edit`/`Write`) share one
|
|
66
|
+
* implementation file (they share a read-snapshot state); the `--tools`
|
|
67
|
+
* allowlist still exposes only the declared subset.
|
|
66
68
|
*/
|
|
67
69
|
const TOOL_EXTENSION_OVERRIDES: Record<string, string> = {
|
|
68
70
|
read: "opencode/read.ts",
|
|
@@ -71,6 +73,9 @@ const TOOL_EXTENSION_OVERRIDES: Record<string, string> = {
|
|
|
71
73
|
bash: "opencode/bash.ts",
|
|
72
74
|
Grep: "claude-code/grep.ts",
|
|
73
75
|
Glob: "claude-code/glob.ts",
|
|
76
|
+
Read: "claude-code/files.ts",
|
|
77
|
+
Edit: "claude-code/files.ts",
|
|
78
|
+
Write: "claude-code/files.ts",
|
|
74
79
|
};
|
|
75
80
|
|
|
76
81
|
// ── schema ───────────────────────────────────────────────────────────────────
|
|
@@ -205,10 +210,17 @@ export function buildSubagentArgs(
|
|
|
205
210
|
const tools = agent.tools ?? DEFAULT_TOOLS;
|
|
206
211
|
// Load the opencode override for each built-in tool the agent declares
|
|
207
212
|
// (read/edit/write), so the subagent uses the enhanced implementation
|
|
208
|
-
// instead of the built-in one.
|
|
213
|
+
// instead of the built-in one. Several tool names can map to the same
|
|
214
|
+
// implementation file (e.g. cc Read/Edit/Write → claude-code/files.ts);
|
|
215
|
+
// loading a file twice would run its extension factory twice and create
|
|
216
|
+
// separate closure states, so each file is loaded at most once.
|
|
217
|
+
const loadedOverrideFiles = new Set<string>();
|
|
209
218
|
for (const tool of tools) {
|
|
210
219
|
const ext = TOOL_EXTENSION_OVERRIDES[tool];
|
|
211
|
-
if (ext
|
|
220
|
+
if (ext && !loadedOverrideFiles.has(ext)) {
|
|
221
|
+
loadedOverrideFiles.add(ext);
|
|
222
|
+
args.push("-e", extensionPath(ext));
|
|
223
|
+
}
|
|
212
224
|
}
|
|
213
225
|
args.push("--tools", tools.join(","));
|
|
214
226
|
if (systemPromptPath) args.push("--append-system-prompt", systemPromptPath);
|