pi-tool-discipline 0.1.7 → 0.1.9
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 +15 -8
- package/extensions/index.ts +9 -26
- package/extensions/search.ts +122 -40
- package/extensions/strip.ts +23 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -28,8 +28,9 @@ Two mechanisms, applied automatically in every session:
|
|
|
28
28
|
`Use bash for file operations like ls, rg, find` guideline is **never
|
|
29
29
|
generated** — the model is never told to use bash for searching. On older
|
|
30
30
|
pi versions without these built-ins, fs-based fallbacks
|
|
31
|
-
(`extensions/search.ts`) are registered instead
|
|
32
|
-
`ffgrep`/`fffind` from pi-fff are
|
|
31
|
+
(`extensions/search.ts`) are registered instead, with their text snippet
|
|
32
|
+
suppressed while `ffgrep`/`fffind` from pi-fff are active (the registered
|
|
33
|
+
tool schema remains present either way).
|
|
33
34
|
2. **System-prompt injection (rules).** On every agent start, appends an
|
|
34
35
|
idempotent "Tool Discipline" section to the system prompt: content search
|
|
35
36
|
with `ffgrep`, path search with `fffind`, file reads with `read`
|
|
@@ -46,8 +47,9 @@ pi install npm:pi-tool-discipline
|
|
|
46
47
|
pi -e npm:pi-tool-discipline
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
Requires `@ff-labs/pi-fff`
|
|
50
|
-
|
|
50
|
+
Requires nothing extra. With `@ff-labs/pi-fff` installed, the model prefers
|
|
51
|
+
`ffgrep`/`fffind`; without it, Pi's built-in `grep`/`find`/`ls` tools (activated
|
|
52
|
+
by this extension, or fs-based fallbacks on older pi versions) are used.
|
|
51
53
|
|
|
52
54
|
## Verify
|
|
53
55
|
|
|
@@ -66,10 +68,15 @@ This extension runs with full system access like any pi extension. What it does:
|
|
|
66
68
|
- On pi versions without built-in search tools, registers read-only fs-based
|
|
67
69
|
fallback implementations that read file contents under the searched path.
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
**Disclosure:** pi's built-in `grep`/`find` tools execute the `rg`/`fd`
|
|
72
|
+
binaries, and pi may auto-download those binaries from GitHub on first use
|
|
73
|
+
(`ensureTool`). This extension itself does not execute commands, write files,
|
|
74
|
+
or touch the network — that claim covers only its own fs-based fallback
|
|
75
|
+
implementations, not the pi built-ins it activates.
|
|
76
|
+
|
|
77
|
+
The fallback search tools only read. Note that any installed tool, including
|
|
78
|
+
this one, can be invoked by the model. Review the source in `extensions/`
|
|
79
|
+
before installing.
|
|
73
80
|
|
|
74
81
|
## License
|
|
75
82
|
|
package/extensions/index.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
29
29
|
import { Type } from "typebox";
|
|
30
30
|
import { grepFiles, findFiles, listDir, grepSchema, findSchema, lsSchema } from "./search.js";
|
|
31
|
+
import { stripBashGuidelines } from "./strip.js";
|
|
31
32
|
|
|
32
33
|
const MARK = "<!-- pi-tool-discipline:v1 -->";
|
|
33
34
|
|
|
@@ -37,24 +38,14 @@ const PLACEHOLDER_NAMES = ["grep", "find", "ls"] as const;
|
|
|
37
38
|
/** FFF search tools that indicate pi-fff (or equivalent) is installed. */
|
|
38
39
|
const FFF_TOOLS = ["ffgrep", "fffind"];
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
-
* The exact guidelines pi's builder injects when no search tool is active.
|
|
42
|
-
* Stripped from the system prompt as a fallback (plan B).
|
|
43
|
-
*/
|
|
44
|
-
const BASH_GUIDELINES = [
|
|
45
|
-
"Use bash for file operations like ls, rg, find",
|
|
46
|
-
"Use bash or PowerShell for file operations like listing, searching, and finding files",
|
|
47
|
-
"Use PowerShell for file operations like listing, searching, and finding files",
|
|
48
|
-
];
|
|
49
|
-
|
|
50
41
|
const DISCIPLINE = `
|
|
51
42
|
## Tool Discipline (pi-tool-discipline)
|
|
52
43
|
|
|
53
44
|
Search tool priority:
|
|
54
45
|
|
|
55
46
|
1. ffgrep / fffind (from @ff-labs/pi-fff) — always preferred. They work with absolute paths outside the workspace and support regex / path / exclude filters.
|
|
56
|
-
2. Without pi-fff, use the grep / find / ls TOOLS (fallbacks
|
|
57
|
-
3. Never run bash \`grep\` or \`find\`. Never use bash \`ls\`/\`cat\`/\`head\`/\`tail\`/\`sed\`/\`which\` directly for searching or reading — use ffgrep / fffind / read (or the grep/find/ls
|
|
47
|
+
2. Without pi-fff, use the grep / find / ls TOOLS (Pi built-ins on pi 0.84+, fs fallbacks on older versions). Fill their parameters according to each tool's declared schema — built-in grep uses ignoreCase/limit, built-in find uses a glob pattern. Never fall back to bash's grep or find.
|
|
48
|
+
3. Never run bash \`grep\` or \`find\`. Never use bash \`ls\`/\`cat\`/\`head\`/\`tail\`/\`sed\`/\`which\` directly for searching or reading — use ffgrep / fffind / read (or the grep/find/ls tools) instead.
|
|
58
49
|
4. Read files with \`read\` (offset/limit for large files).
|
|
59
50
|
5. Bash stays allowed only when dedicated tools cannot do the job: pipelines, git, npm, running programs, network requests, file mutations.
|
|
60
51
|
6. If bash searching is truly unavoidable, use \`rg\` (never \`grep\`).`;
|
|
@@ -64,7 +55,7 @@ interface FallbackTool {
|
|
|
64
55
|
description: string;
|
|
65
56
|
snippet: string;
|
|
66
57
|
parameters: ReturnType<typeof Type.Object>;
|
|
67
|
-
execute: (params: any, cwd: string) => string
|
|
58
|
+
execute: (params: any, cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
68
59
|
}
|
|
69
60
|
|
|
70
61
|
const FALLBACK_TOOLS: Record<string, FallbackTool> = {
|
|
@@ -74,7 +65,7 @@ const FALLBACK_TOOLS: Record<string, FallbackTool> = {
|
|
|
74
65
|
"Search file contents for a text pattern. Fallback for environments without ffgrep; prefer ffgrep when available.",
|
|
75
66
|
snippet: "Search file contents (fallback when ffgrep is unavailable)",
|
|
76
67
|
parameters: grepSchema,
|
|
77
|
-
execute: (params, cwd) => grepFiles({ ...params, cwd }),
|
|
68
|
+
execute: async (params, cwd, signal) => grepFiles({ ...params, cwd, signal }),
|
|
78
69
|
},
|
|
79
70
|
find: {
|
|
80
71
|
label: "find (fallback)",
|
|
@@ -82,7 +73,7 @@ const FALLBACK_TOOLS: Record<string, FallbackTool> = {
|
|
|
82
73
|
"Find files by path/name substring. Fallback for environments without fffind; prefer fffind when available.",
|
|
83
74
|
snippet: "Find files by path/name (fallback when fffind is unavailable)",
|
|
84
75
|
parameters: findSchema,
|
|
85
|
-
execute: (params, cwd) => findFiles({ ...params, cwd }),
|
|
76
|
+
execute: async (params, cwd, signal) => findFiles({ ...params, cwd, signal }),
|
|
86
77
|
},
|
|
87
78
|
ls: {
|
|
88
79
|
label: "ls (fallback)",
|
|
@@ -90,18 +81,10 @@ const FALLBACK_TOOLS: Record<string, FallbackTool> = {
|
|
|
90
81
|
"List directory entries. Fallback for environments without fffind; prefer fffind when available.",
|
|
91
82
|
snippet: "List directory entries (fallback when fffind is unavailable)",
|
|
92
83
|
parameters: lsSchema,
|
|
93
|
-
execute: (params, cwd) => listDir({ ...params, cwd }),
|
|
84
|
+
execute: async (params, cwd, signal) => listDir({ ...params, cwd, signal }),
|
|
94
85
|
},
|
|
95
86
|
};
|
|
96
87
|
|
|
97
|
-
function stripBashGuidelines(prompt: string): string {
|
|
98
|
-
let out = prompt;
|
|
99
|
-
for (const guideline of BASH_GUIDELINES) {
|
|
100
|
-
out = out.replaceAll(guideline, "Use ffgrep/fffind for file operations like ls, rg, find");
|
|
101
|
-
}
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
88
|
export default function toolDiscipline(pi: ExtensionAPI) {
|
|
106
89
|
// A. Register search-tool names so pi stops generating the bash guideline.
|
|
107
90
|
// Done in session_start: action methods (getAllTools/registerTool) are not
|
|
@@ -140,8 +123,8 @@ export default function toolDiscipline(pi: ExtensionAPI) {
|
|
|
140
123
|
"Use ffgrep/fffind when they are available; grep/find/ls are fallbacks only for environments without pi-fff.",
|
|
141
124
|
],
|
|
142
125
|
parameters: fallback.parameters,
|
|
143
|
-
async execute(_toolCallId, params,
|
|
144
|
-
const text = fallback.execute(params ?? {}, execCtx.cwd);
|
|
126
|
+
async execute(_toolCallId, params, signal, _onUpdate, execCtx) {
|
|
127
|
+
const text = await fallback.execute(params ?? {}, execCtx.cwd, signal);
|
|
145
128
|
return {
|
|
146
129
|
content: [{ type: "text", text }],
|
|
147
130
|
details: { fallback: true },
|
package/extensions/search.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fallback search implementations for environments without @ff-labs/pi-fff.
|
|
3
|
-
* Pure Node fs-based; no shell, no external deps.
|
|
3
|
+
* Pure Node fs-based (async, abort-aware); no shell, no external deps.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { readdir, readFile, stat, lstat } from "fs/promises";
|
|
6
6
|
import { join, resolve, relative } from "path";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
8
|
|
|
@@ -12,6 +12,16 @@ const MAX_VISITED = 5000; // total entries touched, bounds slow/odd trees
|
|
|
12
12
|
const MAX_FILE_BYTES = 1024 * 1024; // content search skips files larger than 1 MiB
|
|
13
13
|
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
14
14
|
const TRUNCATE_MARKER = "\n[output truncated]";
|
|
15
|
+
const CAPPED_MARKER = "\n[search capped — traversal stopped early]";
|
|
16
|
+
|
|
17
|
+
/** Cancellation error, matching pi built-in tools ("Operation aborted"). */
|
|
18
|
+
function abortError(): Error {
|
|
19
|
+
return new Error("Operation aborted");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
23
|
+
if (signal?.aborted) throw abortError();
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
interface FileMatch {
|
|
17
27
|
file: string;
|
|
@@ -19,76 +29,121 @@ interface FileMatch {
|
|
|
19
29
|
text: string;
|
|
20
30
|
}
|
|
21
31
|
|
|
32
|
+
interface WalkState {
|
|
33
|
+
visited: number;
|
|
34
|
+
capped: boolean;
|
|
35
|
+
rootError?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
/**
|
|
23
|
-
* Collect regular files under dir (depth-limited, symlink-safe, bounded
|
|
24
|
-
* Only isFile() entries are collected — FIFOs, devices, sockets
|
|
25
|
-
*
|
|
26
|
-
* the
|
|
27
|
-
*
|
|
39
|
+
* Collect regular files under dir (depth-limited, symlink-safe, bounded,
|
|
40
|
+
* abort-aware). Only isFile() entries are collected — FIFOs, devices, sockets
|
|
41
|
+
* would block a read. Individual OS filesystem requests may still be
|
|
42
|
+
* uninterruptible, but the signal is checked before/after every operation.
|
|
43
|
+
* A failure to read the REQUESTED ROOT (depth 0) is recorded in rootError;
|
|
44
|
+
* nested unreadable entries are skipped.
|
|
28
45
|
*/
|
|
29
|
-
function walk(dir: string, out: string[], state:
|
|
30
|
-
|
|
46
|
+
async function walk(dir: string, out: string[], state: WalkState, signal?: AbortSignal, depth = 0): Promise<void> {
|
|
47
|
+
throwIfAborted(signal);
|
|
48
|
+
if (depth > 12 || state.visited >= MAX_VISITED) {
|
|
49
|
+
state.capped = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
31
52
|
let entries: string[];
|
|
32
53
|
try {
|
|
33
|
-
entries =
|
|
34
|
-
} catch {
|
|
54
|
+
entries = await readdir(dir);
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
if (depth === 0) state.rootError = error?.message ?? String(error);
|
|
35
57
|
return;
|
|
36
58
|
}
|
|
59
|
+
throwIfAborted(signal);
|
|
37
60
|
for (const entry of entries) {
|
|
38
|
-
if (out.length >= MAX_FILES || state.visited >= MAX_VISITED)
|
|
61
|
+
if (out.length >= MAX_FILES || state.visited >= MAX_VISITED) {
|
|
62
|
+
state.capped = true;
|
|
63
|
+
return; // in-loop bound
|
|
64
|
+
}
|
|
39
65
|
if (entry.startsWith(".") || SKIP_DIRS.has(entry)) continue;
|
|
40
66
|
const p = join(dir, entry);
|
|
41
67
|
state.visited++;
|
|
42
68
|
try {
|
|
43
|
-
const lst =
|
|
69
|
+
const lst = await lstat(p);
|
|
70
|
+
throwIfAborted(signal);
|
|
44
71
|
if (lst.isSymbolicLink()) continue; // never follow symlinks
|
|
45
|
-
if (lst.isDirectory()) walk(p, out, state, depth + 1);
|
|
72
|
+
if (lst.isDirectory()) await walk(p, out, state, signal, depth + 1);
|
|
46
73
|
else if (lst.isFile()) out.push(p); // regular files only
|
|
47
|
-
} catch {
|
|
74
|
+
} catch (error: any) {
|
|
75
|
+
if (error?.message === "Operation aborted") throw error;
|
|
48
76
|
// unreadable entries are skipped
|
|
49
77
|
}
|
|
50
78
|
}
|
|
51
79
|
}
|
|
52
80
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Truncate by BYTE length, keep complete lines, reserve space for all markers.
|
|
83
|
+
* Returns the original text unchanged when it fits within maxBytes.
|
|
84
|
+
*/
|
|
85
|
+
function truncate(text: string, maxBytes = MAX_OUTPUT_BYTES, extraMarkerBytes = 0): string {
|
|
55
86
|
const buf = Buffer.from(text, "utf8");
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
if (buf.length <= maxBytes) return text;
|
|
88
|
+
const budget = maxBytes - Buffer.byteLength(TRUNCATE_MARKER) - extraMarkerBytes;
|
|
89
|
+
if (budget <= 0) return TRUNCATE_MARKER.trim();
|
|
58
90
|
const cut = buf.subarray(0, budget).toString("utf8");
|
|
59
91
|
const lastNewline = cut.lastIndexOf("\n");
|
|
60
92
|
if (lastNewline <= 0) return TRUNCATE_MARKER.trim(); // nothing complete fits
|
|
61
93
|
return `${cut.slice(0, lastNewline)}\n${TRUNCATE_MARKER}`;
|
|
62
94
|
}
|
|
63
95
|
|
|
64
|
-
|
|
96
|
+
async function resolveRoot(cwd: string, sub?: string): Promise<string | null> {
|
|
97
|
+
const root = resolve(cwd, sub || ".");
|
|
98
|
+
try {
|
|
99
|
+
const st = await stat(root);
|
|
100
|
+
return st.isDirectory() ? root : null;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function walkFiles(root: string, signal?: AbortSignal): Promise<{ files: string[]; state: WalkState }> {
|
|
107
|
+
const files: string[] = [];
|
|
108
|
+
const state: WalkState = { visited: 0, capped: false };
|
|
109
|
+
return walk(root, files, state, signal).then(() => ({ files, state }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function grepFiles(opts: {
|
|
65
113
|
pattern: string;
|
|
66
114
|
path?: string;
|
|
67
115
|
caseSensitive?: boolean;
|
|
68
116
|
maxResults?: number;
|
|
69
117
|
cwd: string;
|
|
70
|
-
|
|
71
|
-
|
|
118
|
+
signal?: AbortSignal;
|
|
119
|
+
}): Promise<string> {
|
|
120
|
+
throwIfAborted(opts.signal);
|
|
121
|
+
const root = await resolveRoot(opts.cwd, opts.path);
|
|
122
|
+
if (!root) return `Error: search path not found: ${resolve(opts.cwd, opts.path || ".")}`;
|
|
72
123
|
const pattern = opts.caseSensitive ? opts.pattern : opts.pattern.toLowerCase();
|
|
73
124
|
const limit = opts.maxResults ?? 100;
|
|
74
|
-
const files
|
|
75
|
-
|
|
125
|
+
const { files, state } = await walkFiles(root, opts.signal);
|
|
126
|
+
if (state.rootError) return `Error: cannot read search root ${root}: ${state.rootError}`;
|
|
76
127
|
const matches: FileMatch[] = [];
|
|
77
128
|
for (const file of files) {
|
|
129
|
+
throwIfAborted(opts.signal);
|
|
78
130
|
if (matches.length >= limit) break;
|
|
79
131
|
try {
|
|
80
|
-
if (
|
|
132
|
+
if ((await stat(file)).size > MAX_FILE_BYTES) continue; // cap only before reading content
|
|
81
133
|
} catch {
|
|
82
134
|
continue;
|
|
83
135
|
}
|
|
136
|
+
throwIfAborted(opts.signal);
|
|
84
137
|
let content: string;
|
|
85
138
|
try {
|
|
86
|
-
content =
|
|
139
|
+
content = await readFile(file, "utf8");
|
|
87
140
|
} catch {
|
|
88
141
|
continue;
|
|
89
142
|
}
|
|
143
|
+
throwIfAborted(opts.signal);
|
|
90
144
|
const lines = content.split("\n");
|
|
91
145
|
for (let i = 0; i < lines.length; i++) {
|
|
146
|
+
throwIfAborted(opts.signal);
|
|
92
147
|
const haystack = opts.caseSensitive ? lines[i] : lines[i].toLowerCase();
|
|
93
148
|
if (haystack.includes(pattern)) {
|
|
94
149
|
matches.push({ file: relative(root, file), line: i + 1, text: lines[i].trim().slice(0, 200) });
|
|
@@ -96,31 +151,58 @@ export function grepFiles(opts: {
|
|
|
96
151
|
}
|
|
97
152
|
}
|
|
98
153
|
}
|
|
99
|
-
if (matches.length === 0) return "No matches found";
|
|
154
|
+
if (matches.length === 0) return state.capped ? `No matches found${CAPPED_MARKER}` : "No matches found";
|
|
100
155
|
let out = "";
|
|
101
156
|
for (const m of matches) out += `${m.file}:${m.line}: ${m.text}\n`;
|
|
102
|
-
|
|
157
|
+
out = truncate(out, MAX_OUTPUT_BYTES, state.capped ? Buffer.byteLength(CAPPED_MARKER) : 0);
|
|
158
|
+
if (state.capped) out += CAPPED_MARKER;
|
|
159
|
+
return out;
|
|
103
160
|
}
|
|
104
161
|
|
|
105
|
-
export function findFiles(opts: {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
162
|
+
export async function findFiles(opts: {
|
|
163
|
+
pattern?: string;
|
|
164
|
+
path?: string;
|
|
165
|
+
maxResults?: number;
|
|
166
|
+
cwd: string;
|
|
167
|
+
signal?: AbortSignal;
|
|
168
|
+
}): Promise<string> {
|
|
169
|
+
throwIfAborted(opts.signal);
|
|
170
|
+
const root = await resolveRoot(opts.cwd, opts.path);
|
|
171
|
+
if (!root) return `Error: search path not found: ${resolve(opts.cwd, opts.path || ".")}`;
|
|
172
|
+
const { files, state } = await walkFiles(root, opts.signal);
|
|
173
|
+
if (state.rootError) return `Error: cannot read search root ${root}: ${state.rootError}`;
|
|
109
174
|
const needle = opts.pattern?.toLowerCase();
|
|
110
175
|
// Match against the RELATIVE path so a pattern matching an ancestor
|
|
111
176
|
// directory does not hit every file, and rendered output stays relative.
|
|
112
|
-
const rel =
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
177
|
+
const rel: string[] = [];
|
|
178
|
+
for (const f of files) {
|
|
179
|
+
throwIfAborted(opts.signal);
|
|
180
|
+
rel.push(relative(root, f));
|
|
181
|
+
}
|
|
182
|
+
const hits: string[] = [];
|
|
183
|
+
for (const r of rel) {
|
|
184
|
+
throwIfAborted(opts.signal);
|
|
185
|
+
if (!needle || r.toLowerCase().includes(needle)) hits.push(r);
|
|
186
|
+
if (hits.length >= (opts.maxResults ?? 100)) break;
|
|
187
|
+
}
|
|
188
|
+
if (hits.length === 0) return state.capped ? `No matching files found${CAPPED_MARKER}` : "No matching files found";
|
|
189
|
+
let out = truncate(hits.join("\n"), MAX_OUTPUT_BYTES, state.capped ? Buffer.byteLength(CAPPED_MARKER) : 0);
|
|
190
|
+
if (state.capped) out += CAPPED_MARKER;
|
|
191
|
+
return out;
|
|
116
192
|
}
|
|
117
193
|
|
|
118
|
-
export function listDir(opts: { path?: string; cwd: string }): string {
|
|
119
|
-
|
|
194
|
+
export async function listDir(opts: { path?: string; cwd: string; signal?: AbortSignal }): Promise<string> {
|
|
195
|
+
throwIfAborted(opts.signal);
|
|
196
|
+
const root = await resolveRoot(opts.cwd, opts.path);
|
|
197
|
+
if (!root) return `Error: directory not found: ${resolve(opts.cwd, opts.path || ".")}`;
|
|
120
198
|
try {
|
|
121
|
-
|
|
199
|
+
const entries = await readdir(root);
|
|
200
|
+
throwIfAborted(opts.signal);
|
|
201
|
+
if (entries.length === 0) return "(empty directory)";
|
|
202
|
+
return truncate(entries.join("\n"));
|
|
122
203
|
} catch (error: any) {
|
|
123
|
-
|
|
204
|
+
if (error?.message === "Operation aborted") throw error;
|
|
205
|
+
return `Error listing ${root}: ${error.message}`;
|
|
124
206
|
}
|
|
125
207
|
}
|
|
126
208
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-line stripping of pi's generated bash file-operation guidelines.
|
|
3
|
+
* Kept dependency-free so it is testable outside the pi runtime.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Strip only the exact generated guideline bullets, line-anchored (whole-line,
|
|
8
|
+
* global), so quoted references inside project instructions or custom prompts
|
|
9
|
+
* are not rewritten. Activation normally prevents pi from generating these
|
|
10
|
+
* anyway; this is a belt-and-suspenders fallback.
|
|
11
|
+
*/
|
|
12
|
+
export function stripBashGuidelines(prompt: string): string {
|
|
13
|
+
return prompt
|
|
14
|
+
.replace(/^- Use bash for file operations like ls, rg, find(?=\r?$)/gm, "- Use ffgrep/fffind for file operations like ls, rg, find")
|
|
15
|
+
.replace(
|
|
16
|
+
/^- Use bash or PowerShell for file operations like listing, searching, and finding files(?=\r?$)/gm,
|
|
17
|
+
"- Use ffgrep/fffind for file operations like listing, searching, and finding files",
|
|
18
|
+
)
|
|
19
|
+
.replace(
|
|
20
|
+
/^- Use PowerShell for file operations like listing, searching, and finding files(?=\r?$)/gm,
|
|
21
|
+
"- Use ffgrep/fffind for file operations like listing, searching, and finding files",
|
|
22
|
+
);
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tool-discipline",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "pi extension: enforce ffgrep/fffind-first search discipline and neutralize the default bash file-operation guideline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,16 +37,17 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"typecheck": "tsc --noEmit"
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "node --experimental-strip-types test/fallback.test.mjs"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
44
|
"@earendil-works/pi-coding-agent": "*",
|
|
44
|
-
"
|
|
45
|
+
"typebox": "*"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@earendil-works/pi-coding-agent": "*",
|
|
48
|
-
"@sinclair/typebox": "*",
|
|
49
49
|
"@types/node": "^22.0.0",
|
|
50
|
+
"typebox": "*",
|
|
50
51
|
"typescript": "^5.0.0"
|
|
51
52
|
}
|
|
52
53
|
}
|