pi-context-filter 1.0.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/LICENSE +21 -0
- package/README.md +104 -0
- package/index.ts +229 -0
- package/package.json +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HazAT
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Context Filter
|
|
2
|
+
|
|
3
|
+
A pi extension that gives you `.gitignore`-style control over which context files, agent instructions, and skills are included in the system prompt.
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
|
|
7
|
+
Pi automatically loads `AGENTS.md` / `CLAUDE.md` files from your home directory, ancestor directories, and the current project. It also loads all discovered skills into the prompt. Sometimes you want to override this — exclude a parent project's instructions when working in a subfolder, skip the global `AGENTS.md`, or inject your own context files.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Place a `.context` file in your project's `.pi/` directory:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
your-project/
|
|
15
|
+
├── .pi/
|
|
16
|
+
│ └── .context
|
|
17
|
+
└── ...
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Pi reads this file on session start. Use `/reload` to pick up changes without restarting.
|
|
21
|
+
|
|
22
|
+
## Syntax
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Lines starting with # are comments
|
|
26
|
+
# Blank lines are ignored
|
|
27
|
+
|
|
28
|
+
# Exclude files with ! (supports globs)
|
|
29
|
+
!~/.pi/agent/AGENTS.md
|
|
30
|
+
|
|
31
|
+
# Include files with + (resolved relative to project root)
|
|
32
|
+
+.pi/MYCONTEXT.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Exclusions (`!`)
|
|
36
|
+
|
|
37
|
+
Exclude `AGENTS.md` / `CLAUDE.md` sections and skill entries from the system prompt. Patterns are matched against absolute file paths using [picomatch](https://github.com/micromatch/picomatch) glob syntax.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Exact path (~ expands to home directory)
|
|
41
|
+
!~/.pi/agent/AGENTS.md
|
|
42
|
+
|
|
43
|
+
# All AGENTS.md files from any ancestor directory
|
|
44
|
+
!**/AGENTS.md
|
|
45
|
+
|
|
46
|
+
# A specific skill
|
|
47
|
+
!**/skills/brainstorm/SKILL.md
|
|
48
|
+
|
|
49
|
+
# All skills under a directory
|
|
50
|
+
!**/skills/**
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Inclusions (`+`)
|
|
54
|
+
|
|
55
|
+
Add arbitrary files to the system prompt. Paths are resolved relative to the project root. Files are read fresh on every prompt, so edits are picked up immediately.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Add a custom context file
|
|
59
|
+
+.pi/MYCONTEXT.md
|
|
60
|
+
|
|
61
|
+
# Absolute paths work too
|
|
62
|
+
+~/shared-context/team-guidelines.md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
**Use only the local project's AGENTS.md, ignore everything else:**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# .pi/.context
|
|
71
|
+
!~/.pi/agent/AGENTS.md
|
|
72
|
+
!../**/AGENTS.md
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Swap in your own context file:**
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# .pi/.context
|
|
79
|
+
!**/AGENTS.md
|
|
80
|
+
+.pi/my-instructions.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Keep everything but drop a noisy skill:**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# .pi/.context
|
|
87
|
+
!**/skills/brainstorm/SKILL.md
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## How It Works
|
|
91
|
+
|
|
92
|
+
The extension hooks into pi's `before_agent_start` event to modify the system prompt before each LLM call:
|
|
93
|
+
|
|
94
|
+
1. **Exclusions** — Strips `## /path/to/AGENTS.md` sections from the Project Context block and removes matching `<skill>` entries from the available skills list.
|
|
95
|
+
2. **Inclusions** — Reads the specified files and appends them to the Project Context section.
|
|
96
|
+
|
|
97
|
+
On startup, a widget above the editor shows all active rules (auto-hides after 15 seconds), and a persistent footer status displays the filter summary (e.g., `[.context] ✕ 5 excluded`).
|
|
98
|
+
|
|
99
|
+
Note: Pi's own `[Context]` and `[Skills]` startup display still shows all discovered files — that's pi's internal display and can't be changed by extensions. The widget and footer make it clear what's actually being filtered from the prompt.
|
|
100
|
+
|
|
101
|
+
## Limitations
|
|
102
|
+
|
|
103
|
+
- **Extensions cannot be filtered.** Extension loading happens before any extension code runs, so this tool can only control what appears in the *system prompt* (AGENTS.md content and skill listings).
|
|
104
|
+
- **Format-dependent.** The extension parses the system prompt structure that pi generates. If pi significantly changes its prompt format in the future, the filtering may need updating.
|
package/index.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import picomatch from "picomatch";
|
|
6
|
+
|
|
7
|
+
interface ContextRules {
|
|
8
|
+
exclusionPatterns: string[]; // original patterns for display
|
|
9
|
+
exclusions: picomatch.Matcher[];
|
|
10
|
+
inclusions: string[]; // resolved absolute paths
|
|
11
|
+
inclusionPatterns: string[]; // original patterns for display
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function expandTilde(p: string): string {
|
|
15
|
+
if (p === "~") return homedir();
|
|
16
|
+
if (p.startsWith("~/")) return join(homedir(), p.slice(2));
|
|
17
|
+
return p;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseContextFile(filePath: string, cwd: string): ContextRules {
|
|
21
|
+
const rules: ContextRules = {
|
|
22
|
+
exclusionPatterns: [],
|
|
23
|
+
exclusions: [],
|
|
24
|
+
inclusions: [],
|
|
25
|
+
inclusionPatterns: [],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (!existsSync(filePath)) return rules;
|
|
29
|
+
|
|
30
|
+
const content = readFileSync(filePath, "utf-8");
|
|
31
|
+
for (const rawLine of content.split("\n")) {
|
|
32
|
+
const line = rawLine.trim();
|
|
33
|
+
if (!line || line.startsWith("#")) continue;
|
|
34
|
+
|
|
35
|
+
if (line.startsWith("!")) {
|
|
36
|
+
const raw = line.slice(1).trim();
|
|
37
|
+
const pattern = expandTilde(raw);
|
|
38
|
+
rules.exclusionPatterns.push(raw);
|
|
39
|
+
rules.exclusions.push(picomatch(pattern, { dot: true }));
|
|
40
|
+
} else if (line.startsWith("+")) {
|
|
41
|
+
const raw = line.slice(1).trim();
|
|
42
|
+
const rawPath = expandTilde(raw);
|
|
43
|
+
rules.inclusionPatterns.push(raw);
|
|
44
|
+
rules.inclusions.push(resolve(cwd, rawPath));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return rules;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isExcluded(filePath: string, matchers: picomatch.Matcher[]): boolean {
|
|
52
|
+
return matchers.some((m) => m(filePath));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Remove ## path sections from the "# Project Context" area of the system prompt.
|
|
57
|
+
* Sections look like: ## /absolute/path/to/AGENTS.md\n\n<content>\n\n
|
|
58
|
+
*/
|
|
59
|
+
function filterContextSections(prompt: string, matchers: picomatch.Matcher[]): string {
|
|
60
|
+
if (matchers.length === 0) return prompt;
|
|
61
|
+
|
|
62
|
+
// Match sections: ## <path>\n\n<content up to next ## or end of Project Context>
|
|
63
|
+
// The Project Context block starts with "# Project Context"
|
|
64
|
+
const projectCtxStart = prompt.indexOf("# Project Context");
|
|
65
|
+
if (projectCtxStart === -1) return prompt;
|
|
66
|
+
|
|
67
|
+
// Find where Project Context ends (next top-level heading or skills section)
|
|
68
|
+
const afterCtx = prompt.indexOf("\n\nThe following skills", projectCtxStart);
|
|
69
|
+
const afterDate = prompt.indexOf("\nCurrent date and time:", projectCtxStart);
|
|
70
|
+
|
|
71
|
+
let projectCtxEnd: number;
|
|
72
|
+
if (afterCtx !== -1) {
|
|
73
|
+
projectCtxEnd = afterCtx;
|
|
74
|
+
} else if (afterDate !== -1) {
|
|
75
|
+
projectCtxEnd = afterDate;
|
|
76
|
+
} else {
|
|
77
|
+
projectCtxEnd = prompt.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const before = prompt.slice(0, projectCtxStart);
|
|
81
|
+
const contextBlock = prompt.slice(projectCtxStart, projectCtxEnd);
|
|
82
|
+
const after = prompt.slice(projectCtxEnd);
|
|
83
|
+
|
|
84
|
+
// Split context block into sections by ## /path headers only.
|
|
85
|
+
// AGENTS.md content contains its own ## headings, so we must only match
|
|
86
|
+
// headers that look like file paths (start with /).
|
|
87
|
+
const sectionRegex = /^## (\/[^\n]+)$/gm;
|
|
88
|
+
const sections: { path: string; start: number; end: number }[] = [];
|
|
89
|
+
let match: RegExpExecArray | null;
|
|
90
|
+
|
|
91
|
+
while ((match = sectionRegex.exec(contextBlock)) !== null) {
|
|
92
|
+
if (sections.length > 0) {
|
|
93
|
+
sections[sections.length - 1].end = match.index;
|
|
94
|
+
}
|
|
95
|
+
sections.push({ path: match[1].trim(), start: match.index, end: contextBlock.length });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build filtered context block
|
|
99
|
+
let filtered = contextBlock;
|
|
100
|
+
// Process in reverse to preserve indices
|
|
101
|
+
for (let i = sections.length - 1; i >= 0; i--) {
|
|
102
|
+
const section = sections[i];
|
|
103
|
+
if (isExcluded(section.path, matchers)) {
|
|
104
|
+
filtered = filtered.slice(0, section.start) + filtered.slice(section.end);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// If all sections were removed, remove the entire Project Context header too
|
|
109
|
+
const hasAnySections = /^## /m.test(filtered);
|
|
110
|
+
if (!hasAnySections) {
|
|
111
|
+
return before.trimEnd() + after;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return before + filtered + after;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Remove <skill> blocks whose <location> matches an exclusion pattern.
|
|
119
|
+
*/
|
|
120
|
+
function filterSkillBlocks(prompt: string, matchers: picomatch.Matcher[]): string {
|
|
121
|
+
if (matchers.length === 0) return prompt;
|
|
122
|
+
|
|
123
|
+
return prompt.replace(
|
|
124
|
+
/ <skill>\n(?:.*\n)*? <\/skill>/g,
|
|
125
|
+
(block) => {
|
|
126
|
+
const locMatch = block.match(/<location>([^<]+)<\/location>/);
|
|
127
|
+
if (locMatch && isExcluded(locMatch[1], matchers)) {
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
return block;
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Append included files to the system prompt in the Project Context section.
|
|
137
|
+
*/
|
|
138
|
+
function appendInclusions(
|
|
139
|
+
prompt: string,
|
|
140
|
+
inclusions: string[],
|
|
141
|
+
notify: (msg: string) => void
|
|
142
|
+
): string {
|
|
143
|
+
if (inclusions.length === 0) return prompt;
|
|
144
|
+
|
|
145
|
+
let additions = "";
|
|
146
|
+
for (const filePath of inclusions) {
|
|
147
|
+
if (!existsSync(filePath)) {
|
|
148
|
+
notify(`context-filter: missing inclusion file: ${filePath}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const content = readFileSync(filePath, "utf-8");
|
|
153
|
+
additions += `## ${filePath}\n\n${content}\n\n`;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
notify(`context-filter: failed to read ${filePath}: ${e}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!additions) return prompt;
|
|
160
|
+
|
|
161
|
+
// Insert before skills section or date line
|
|
162
|
+
const skillsIdx = prompt.indexOf("\n\nThe following skills");
|
|
163
|
+
const dateIdx = prompt.indexOf("\nCurrent date and time:");
|
|
164
|
+
const insertIdx = skillsIdx !== -1 ? skillsIdx : dateIdx !== -1 ? dateIdx : prompt.length;
|
|
165
|
+
|
|
166
|
+
// Ensure there's a Project Context section
|
|
167
|
+
const hasProjectCtx = prompt.includes("# Project Context");
|
|
168
|
+
if (!hasProjectCtx) {
|
|
169
|
+
additions = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n" + additions;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return prompt.slice(0, insertIdx) + additions + prompt.slice(insertIdx);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default function (pi: ExtensionAPI) {
|
|
176
|
+
let rules: ContextRules = { exclusions: [], inclusions: [] };
|
|
177
|
+
let cwd = process.cwd();
|
|
178
|
+
|
|
179
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
180
|
+
cwd = ctx.cwd;
|
|
181
|
+
const contextPath = join(cwd, ".pi", ".context");
|
|
182
|
+
rules = parseContextFile(contextPath, cwd);
|
|
183
|
+
|
|
184
|
+
if (!ctx.hasUI) return;
|
|
185
|
+
if (rules.exclusions.length === 0 && rules.inclusions.length === 0) return;
|
|
186
|
+
|
|
187
|
+
// Show summary in footer status
|
|
188
|
+
const parts: string[] = [];
|
|
189
|
+
if (rules.exclusionPatterns.length > 0) {
|
|
190
|
+
parts.push(`✕ ${rules.exclusionPatterns.length} excluded`);
|
|
191
|
+
}
|
|
192
|
+
if (rules.inclusionPatterns.length > 0) {
|
|
193
|
+
parts.push(`✓ ${rules.inclusionPatterns.length} included`);
|
|
194
|
+
}
|
|
195
|
+
ctx.ui.setStatus("context-filter", `[.context] ${parts.join(", ")}`);
|
|
196
|
+
|
|
197
|
+
// Show detailed widget above editor
|
|
198
|
+
const lines: string[] = ["[Context Filter]"];
|
|
199
|
+
for (const p of rules.exclusionPatterns) {
|
|
200
|
+
lines.push(` ✕ ${p}`);
|
|
201
|
+
}
|
|
202
|
+
for (const p of rules.inclusionPatterns) {
|
|
203
|
+
lines.push(` ✓ ${p}`);
|
|
204
|
+
}
|
|
205
|
+
ctx.ui.setWidget("context-filter", lines);
|
|
206
|
+
|
|
207
|
+
// Auto-hide widget after 15 seconds, keep status
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
ctx.ui.setWidget("context-filter", undefined);
|
|
210
|
+
}, 15_000);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
214
|
+
if (rules.exclusions.length === 0 && rules.inclusions.length === 0) return;
|
|
215
|
+
|
|
216
|
+
let prompt = event.systemPrompt;
|
|
217
|
+
|
|
218
|
+
// Apply exclusions
|
|
219
|
+
prompt = filterContextSections(prompt, rules.exclusions);
|
|
220
|
+
prompt = filterSkillBlocks(prompt, rules.exclusions);
|
|
221
|
+
|
|
222
|
+
// Apply inclusions
|
|
223
|
+
prompt = appendInclusions(prompt, rules.inclusions, (msg) => {
|
|
224
|
+
if (ctx.hasUI) ctx.ui.notify(msg, "warning");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return { systemPrompt: prompt };
|
|
228
|
+
});
|
|
229
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-context-filter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A pi extension that provides .gitignore-style control over which context files and skills appear in the system prompt",
|
|
5
|
+
"keywords": ["pi-package"],
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/HazAT/pi-context-filter"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"pi": {
|
|
12
|
+
"extensions": ["./index.ts"]
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"picomatch": "^4.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/picomatch": "^3.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|