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.
Files changed (4) hide show
  1. package/README.md +59 -0
  2. package/index.ts +185 -0
  3. package/package.json +42 -0
  4. 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