pi-diet-ripgrep 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/index.ts +185 -0
- package/package.json +42 -0
- package/spec.md +174 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# pi-diet-ripgrep
|
|
2
|
+
|
|
3
|
+
A minimal Pi extension that exposes ripgrep as a first-class exact-search tool.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `index.ts` — extension entrypoint
|
|
8
|
+
- `spec.md` — design spec
|
|
9
|
+
- `scripts/estimate_prompt_footprint.py` — rough prompt-cost estimator
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
After the package is published to npm:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install npm:pi-diet-ripgrep
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
For a pinned GitHub install:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pi install git:github.com/ProbabilityEngineer/pi-diet-ripgrep@v0.1.0
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For local development:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pi -e /path/to/pi-diet-ripgrep/index.ts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or symlink/copy into an auto-discovered extension location:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
mkdir -p ~/.pi/agent/extensions/pi-diet-ripgrep
|
|
35
|
+
ln -sf /path/to/pi-diet-ripgrep/index.ts ~/.pi/agent/extensions/pi-diet-ripgrep/index.ts
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Publishing
|
|
39
|
+
|
|
40
|
+
First publish manually once:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm publish --access public
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then configure npm Trusted Publishing for GitHub Actions on npmjs.com using this repository and `.github/workflows/npm-publish.yml`.
|
|
47
|
+
|
|
48
|
+
After trusted publishing is configured, publish future versions by bumping `package.json` and pushing a matching version tag:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm version patch --no-git-tag-version
|
|
52
|
+
git commit -am "Release v0.1.1"
|
|
53
|
+
git tag v0.1.1
|
|
54
|
+
git push && git push origin v0.1.1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Tool
|
|
58
|
+
|
|
59
|
+
`rg` is intended for exact text and regex search only.
|
package/index.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_MAX_BYTES,
|
|
9
|
+
DEFAULT_MAX_LINES,
|
|
10
|
+
defineTool,
|
|
11
|
+
formatSize,
|
|
12
|
+
truncateHead,
|
|
13
|
+
type TruncationResult,
|
|
14
|
+
withFileMutationQueue,
|
|
15
|
+
type ExtensionAPI,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
const execFile = promisify(execFileCallback);
|
|
19
|
+
|
|
20
|
+
const rgTool = defineTool({
|
|
21
|
+
name: "rg",
|
|
22
|
+
label: "ripgrep",
|
|
23
|
+
description: "Exact text/regex search with ripgrep. Use for literal verification, logs, config keys, TODOs, and message lookup. Prefer LSP/AST/Semble for symbol, structural, or semantic discovery.",
|
|
24
|
+
parameters: Type.Object({
|
|
25
|
+
query: Type.String({ description: "Pattern" }),
|
|
26
|
+
paths: Type.Optional(Type.Array(Type.String({ description: "Path" }))),
|
|
27
|
+
glob: Type.Optional(Type.Array(Type.String({ description: "Glob" }))),
|
|
28
|
+
caseSensitive: Type.Optional(Type.Boolean({ description: "Case-sensitive" })),
|
|
29
|
+
word: Type.Optional(Type.Boolean({ description: "Word match" })),
|
|
30
|
+
context: Type.Optional(Type.Integer({ minimum: 0, description: "Context lines" })),
|
|
31
|
+
maxMatches: Type.Optional(Type.Integer({ minimum: 1, maximum: 200, description: "Match cap" })),
|
|
32
|
+
hidden: Type.Optional(Type.Boolean({ description: "Search hidden files" })),
|
|
33
|
+
fixedStrings: Type.Optional(Type.Boolean({ description: "Literal strings" })),
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
37
|
+
const args = buildArgs(params);
|
|
38
|
+
const searchPaths = params.paths?.length ? params.paths : ["."];
|
|
39
|
+
const maxMatches = params.maxMatches ?? 50;
|
|
40
|
+
|
|
41
|
+
let stdout = "";
|
|
42
|
+
try {
|
|
43
|
+
const result = await execFile("rg", [...args, params.query, ...searchPaths], {
|
|
44
|
+
cwd: ctx.cwd,
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
maxBuffer: 100 * 1024 * 1024,
|
|
47
|
+
});
|
|
48
|
+
stdout = result.stdout;
|
|
49
|
+
} catch (error: any) {
|
|
50
|
+
stdout = error?.stdout ?? "";
|
|
51
|
+
if (error?.code !== 1) {
|
|
52
|
+
throw new Error(error?.stderr?.trim() || error?.message || "ripgrep failed");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const parsed = parseRgJson(stdout, maxMatches);
|
|
57
|
+
const details: RgDetails = {
|
|
58
|
+
query: params.query,
|
|
59
|
+
searchedPaths: searchPaths,
|
|
60
|
+
matchCount: parsed.totalMatchCount,
|
|
61
|
+
truncated: parsed.totalMatchCount > parsed.matches.length,
|
|
62
|
+
matches: parsed.matches,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (!parsed.totalMatchCount) {
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
68
|
+
details,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const fullText = parsed.lines.join("\n");
|
|
73
|
+
const truncation = truncateHead(fullText, {
|
|
74
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
75
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let text = truncation.content;
|
|
79
|
+
if (details.truncated || truncation.truncated) {
|
|
80
|
+
details.truncated = true;
|
|
81
|
+
details.truncation = truncation;
|
|
82
|
+
const tempDir = await mkdtemp(join(tmpdir(), "pi-diet-ripgrep-"));
|
|
83
|
+
const tempFile = join(tempDir, "output.txt");
|
|
84
|
+
await withFileMutationQueue(tempFile, async () => {
|
|
85
|
+
await writeFile(tempFile, fullText, "utf8");
|
|
86
|
+
});
|
|
87
|
+
details.fullOutputPath = tempFile;
|
|
88
|
+
const omittedMatches = Math.max(0, parsed.totalMatchCount - parsed.matches.length);
|
|
89
|
+
const notices: string[] = [];
|
|
90
|
+
if (omittedMatches) notices.push(`${omittedMatches} matches omitted by maxMatches cap`);
|
|
91
|
+
if (truncation.truncated) {
|
|
92
|
+
notices.push(
|
|
93
|
+
`output truncated to ${truncation.outputLines}/${truncation.totalLines} lines and ${formatSize(truncation.outputBytes)}/${formatSize(truncation.totalBytes)}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
text += `\n\n[${notices.join("; ")}. Full output: ${tempFile}]`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text }],
|
|
101
|
+
details,
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export default function (pi: ExtensionAPI) {
|
|
107
|
+
pi.registerTool(rgTool);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function buildArgs(params: {
|
|
111
|
+
glob?: string[];
|
|
112
|
+
caseSensitive?: boolean;
|
|
113
|
+
word?: boolean;
|
|
114
|
+
context?: number;
|
|
115
|
+
hidden?: boolean;
|
|
116
|
+
fixedStrings?: boolean;
|
|
117
|
+
}) {
|
|
118
|
+
const args = ["--json", "--color=never", "--line-number", "--column"];
|
|
119
|
+
if (params.caseSensitive) {
|
|
120
|
+
args.push("--case-sensitive");
|
|
121
|
+
} else {
|
|
122
|
+
args.push("--smart-case");
|
|
123
|
+
}
|
|
124
|
+
if (params.word) args.push("-w");
|
|
125
|
+
if (params.hidden) args.push("--hidden");
|
|
126
|
+
if (params.fixedStrings) args.push("--fixed-strings");
|
|
127
|
+
if (typeof params.context === "number") args.push("-C", String(params.context));
|
|
128
|
+
for (const glob of params.glob ?? []) args.push("--glob", glob);
|
|
129
|
+
return args;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type RgMatch = {
|
|
133
|
+
path: string;
|
|
134
|
+
line: number;
|
|
135
|
+
column: number;
|
|
136
|
+
text: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
type RgDetails = {
|
|
140
|
+
query: string;
|
|
141
|
+
searchedPaths: string[];
|
|
142
|
+
matchCount: number;
|
|
143
|
+
truncated: boolean;
|
|
144
|
+
matches: RgMatch[];
|
|
145
|
+
truncation?: TruncationResult;
|
|
146
|
+
fullOutputPath?: string;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
function parseRgJson(stdout: string, maxMatches: number) {
|
|
150
|
+
const lines: string[] = [];
|
|
151
|
+
const matches: RgMatch[] = [];
|
|
152
|
+
let totalMatchCount = 0;
|
|
153
|
+
|
|
154
|
+
for (const rawLine of stdout.split(/\r?\n/)) {
|
|
155
|
+
if (!rawLine.trim()) continue;
|
|
156
|
+
let record: any;
|
|
157
|
+
try {
|
|
158
|
+
record = JSON.parse(rawLine);
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (record.type === "match") {
|
|
164
|
+
totalMatchCount += 1;
|
|
165
|
+
const path = record.data?.path?.text ?? "<unknown>";
|
|
166
|
+
const line = record.data?.line_number ?? 0;
|
|
167
|
+
const text = String(record.data?.lines?.text ?? "").replace(/\n$/, "");
|
|
168
|
+
const column = (record.data?.submatches?.[0]?.start ?? 0) + 1;
|
|
169
|
+
if (matches.length < maxMatches) {
|
|
170
|
+
matches.push({ path, line, column, text });
|
|
171
|
+
lines.push(`${path}:${line}:${column}:${text}`);
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (record.type === "context" && matches.length > 0) {
|
|
177
|
+
const path = record.data?.path?.text ?? "<unknown>";
|
|
178
|
+
const line = record.data?.line_number ?? 0;
|
|
179
|
+
const text = String(record.data?.lines?.text ?? "").replace(/\n$/, "");
|
|
180
|
+
lines.push(`${path}-${line}- ${text}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { lines, matches, totalMatchCount };
|
|
185
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-diet-ripgrep",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal Pi extension exposing ripgrep as a first-class exact-search tool.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts",
|
|
9
|
+
"README.md",
|
|
10
|
+
"spec.md"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi-package",
|
|
14
|
+
"pi",
|
|
15
|
+
"pi-extension",
|
|
16
|
+
"ripgrep",
|
|
17
|
+
"rg",
|
|
18
|
+
"search"
|
|
19
|
+
],
|
|
20
|
+
"author": "ProbabilityEngineer",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+ssh://git@github.com/ProbabilityEngineer/pi-diet-ripgrep.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/ProbabilityEngineer/pi-diet-ripgrep/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/ProbabilityEngineer/pi-diet-ripgrep#readme",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@earendil-works/pi-ai": "*",
|
|
35
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
36
|
+
},
|
|
37
|
+
"pi": {
|
|
38
|
+
"extensions": [
|
|
39
|
+
"./index.ts"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
}
|
package/spec.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# pi-diet-ripgrep spec
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
`pi-diet-ripgrep` is a minimal Pi extension that exposes ripgrep as a first-class tool for exact text and regex search with a small prompt footprint.
|
|
6
|
+
|
|
7
|
+
The extension is intentionally "diet": it should add as little system-prompt overhead as possible while still being useful enough to prefer over raw `bash "rg ..."` for common literal-search workflows.
|
|
8
|
+
|
|
9
|
+
## Problem
|
|
10
|
+
|
|
11
|
+
Pi already supports:
|
|
12
|
+
- LSP for symbol-aware navigation
|
|
13
|
+
- AST tools for structural search
|
|
14
|
+
- Semble for semantic discovery
|
|
15
|
+
- `bash` for ad hoc `rg`
|
|
16
|
+
|
|
17
|
+
What is missing is a compact, structured, exact-text search tool that:
|
|
18
|
+
- is cheaper and safer than freeform shell use
|
|
19
|
+
- has consistent defaults
|
|
20
|
+
- returns structured results
|
|
21
|
+
- preserves a clear routing boundary: use `rg` for exact text, not semantic or structural discovery
|
|
22
|
+
|
|
23
|
+
## Goals
|
|
24
|
+
|
|
25
|
+
1. Expose a first-class ripgrep tool for exact text/regex search.
|
|
26
|
+
2. Keep added prompt footprint to roughly 100-200 tokens, ideally under 200.
|
|
27
|
+
3. Return concise structured output suitable for agent use.
|
|
28
|
+
4. Bound output size and support truncation safely.
|
|
29
|
+
5. Avoid duplicating guidance already covered by Pi's built-in search/routing instructions.
|
|
30
|
+
|
|
31
|
+
## Non-goals
|
|
32
|
+
|
|
33
|
+
- Replacing LSP, AST, or semantic tools.
|
|
34
|
+
- Becoming a full grep UI.
|
|
35
|
+
- Supporting every ripgrep flag.
|
|
36
|
+
- Adding large prompt instructions, examples, or verbose routing prose.
|
|
37
|
+
|
|
38
|
+
## Intended use
|
|
39
|
+
|
|
40
|
+
Use `rg` when the task needs:
|
|
41
|
+
- exact literals
|
|
42
|
+
- regex search
|
|
43
|
+
- finding strings, log messages, config keys, TODOs, identifiers by text
|
|
44
|
+
- quick scoped verification after semantic/AST discovery
|
|
45
|
+
|
|
46
|
+
Do not prefer `rg` for:
|
|
47
|
+
- definitions/references/types
|
|
48
|
+
- structural code patterns
|
|
49
|
+
- semantic behavior discovery
|
|
50
|
+
|
|
51
|
+
## Tool shape
|
|
52
|
+
|
|
53
|
+
### Name
|
|
54
|
+
|
|
55
|
+
`rg`
|
|
56
|
+
|
|
57
|
+
### Description
|
|
58
|
+
|
|
59
|
+
Short form only, e.g.:
|
|
60
|
+
|
|
61
|
+
> Exact text/regex search with ripgrep. Use for literal search, not semantic or structural discovery.
|
|
62
|
+
|
|
63
|
+
### Parameters
|
|
64
|
+
|
|
65
|
+
Keep the schema compact. Initial proposal:
|
|
66
|
+
|
|
67
|
+
- `query: string` — pattern to search for
|
|
68
|
+
- `paths?: string[]` — files/directories to search
|
|
69
|
+
- `glob?: string[]` — include globs
|
|
70
|
+
- `caseSensitive?: boolean`
|
|
71
|
+
- `word?: boolean` — word-boundary match
|
|
72
|
+
- `context?: number` — surrounding lines
|
|
73
|
+
- `maxMatches?: number` — hard cap on returned matches
|
|
74
|
+
|
|
75
|
+
Included in v1 because the prompt cost is acceptable:
|
|
76
|
+
- `hidden?: boolean` — include hidden files when explicitly requested
|
|
77
|
+
- `fixedStrings?: boolean` — treat query as a literal string instead of regex
|
|
78
|
+
|
|
79
|
+
## Output
|
|
80
|
+
|
|
81
|
+
Return:
|
|
82
|
+
- human-readable concise text summary
|
|
83
|
+
- structured details with matches
|
|
84
|
+
|
|
85
|
+
Suggested details shape:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"query": "TODO",
|
|
90
|
+
"searchedPaths": ["."],
|
|
91
|
+
"matchCount": 3,
|
|
92
|
+
"truncated": false,
|
|
93
|
+
"matches": [
|
|
94
|
+
{
|
|
95
|
+
"path": "src/a.ts",
|
|
96
|
+
"line": 12,
|
|
97
|
+
"column": 4,
|
|
98
|
+
"text": "// TODO: tighten validation"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Execution behavior
|
|
105
|
+
|
|
106
|
+
Use `rg --json` under the hood so results are structured and easier to parse safely.
|
|
107
|
+
|
|
108
|
+
Default behavior:
|
|
109
|
+
- no color
|
|
110
|
+
- line numbers included
|
|
111
|
+
- column included when available
|
|
112
|
+
- cwd defaults to current Pi cwd
|
|
113
|
+
- if `paths` omitted, search `.`
|
|
114
|
+
|
|
115
|
+
## Truncation and limits
|
|
116
|
+
|
|
117
|
+
The tool must hard-bound output.
|
|
118
|
+
|
|
119
|
+
Initial rules:
|
|
120
|
+
- truncate text output at Pi's normal custom-tool limits
|
|
121
|
+
- cap returned structured matches with `maxMatches` defaulting to a small value, e.g. 50
|
|
122
|
+
- when truncated, say so explicitly
|
|
123
|
+
- if full output is preserved, write it to a temp file and report the path
|
|
124
|
+
|
|
125
|
+
## Prompt budget
|
|
126
|
+
|
|
127
|
+
Budget target:
|
|
128
|
+
- ideal: <= 150 tokens
|
|
129
|
+
- acceptable: <= 200 tokens
|
|
130
|
+
- avoid: > 250 tokens
|
|
131
|
+
|
|
132
|
+
Ways to stay within budget:
|
|
133
|
+
- one-sentence description
|
|
134
|
+
- short parameter descriptions
|
|
135
|
+
- no embedded examples in tool description
|
|
136
|
+
- no duplicated search-routing rules beyond one brief sentence
|
|
137
|
+
|
|
138
|
+
## Why this is justified
|
|
139
|
+
|
|
140
|
+
This tool should be used frequently enough for:
|
|
141
|
+
- exact literal search
|
|
142
|
+
- regex search
|
|
143
|
+
- log/error/message lookup
|
|
144
|
+
- config-key discovery
|
|
145
|
+
- post-discovery verification
|
|
146
|
+
|
|
147
|
+
That usage is common enough to justify a small first-class tool, but not a verbose extension.
|
|
148
|
+
|
|
149
|
+
## Success criteria
|
|
150
|
+
|
|
151
|
+
1. Tool prompt footprint stays within the budget.
|
|
152
|
+
2. Agent can use `rg` instead of raw `bash` for common exact-search tasks.
|
|
153
|
+
3. Results are safer and easier to parse than shell output.
|
|
154
|
+
4. The extension does not encourage misuse for semantic or structural search.
|
|
155
|
+
5. Implementation remains small and maintainable.
|
|
156
|
+
|
|
157
|
+
## Open questions
|
|
158
|
+
|
|
159
|
+
1. Should `fixedStrings` be included in v1 or omitted to save prompt/schema budget?
|
|
160
|
+
2. Should `hidden` be included in v1 or left to future expansion?
|
|
161
|
+
3. Should the tool expose only summary + first N matches, or always store full JSON in a temp file on truncation?
|
|
162
|
+
4. Should the extension also add a tiny prompt append, or rely entirely on the tool description?
|
|
163
|
+
|
|
164
|
+
## Recommended v1
|
|
165
|
+
|
|
166
|
+
Build the richer-but-still-diet version:
|
|
167
|
+
- `rg` tool only
|
|
168
|
+
- compact schema with `hidden` and `fixedStrings`
|
|
169
|
+
- routing-aware description that defers to LSP/AST/Semble
|
|
170
|
+
- `rg --json`
|
|
171
|
+
- structured matches
|
|
172
|
+
- small default result cap
|
|
173
|
+
- explicit truncation handling
|
|
174
|
+
- no extra prompt append unless measurement shows it is necessary
|