pi-local-agents-only 0.1.4 → 0.1.5

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/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.5 - 2026-04-07
4
+
5
+ - strip already-loaded global `AGENTS.md` / `CLAUDE.md` context reliably instead of rereading live files from disk
6
+ - remove the now-empty `# Project Context` section when only global context was loaded
7
+ - handle prompts whose custom prompt text also mentions the `# Project Context` heading
8
+ - make `/local-agents-only off` report when the repo is still enabled via the global allowlist or `PI_LOCAL_AGENTS_ONLY`
9
+ - document the repo-marker-only behavior of `/local-agents-only off`
10
+ - add integration tests against pi's real system prompt builder and command UX regressions
package/README.md CHANGED
@@ -30,6 +30,8 @@ Disable for the current repo:
30
30
  /local-agents-only off
31
31
  ```
32
32
 
33
+ `/local-agents-only off` clears the repo marker only. If the repo is still enabled via `/local-agents-only global-on` or `PI_LOCAL_AGENTS_ONLY=1`, it remains enabled until you also run `/local-agents-only global-off` or unset the env var.
34
+
33
35
  Enable or disable via the global allowlist:
34
36
 
35
37
  ```bash
@@ -16,6 +16,10 @@ const MARKER = join(".pi", COMMAND);
16
16
  const GLOBAL_CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md"];
17
17
  const ENV_TRUE = ["1", "true", "yes", "on"];
18
18
  const ENV_FALSE = ["0", "false", "no", "off"];
19
+ const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
20
+ const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
21
+ const DATE_HEADER = "\nCurrent date:";
22
+ const CONTEXT_BLOCK_HEADER = /^## ([^\n]+(?:AGENTS|CLAUDE)\.md)\n\n/gm;
19
23
 
20
24
  const getAgentDir = () => {
21
25
  const env = process.env.PI_CODING_AGENT_DIR;
@@ -89,10 +93,59 @@ const getEnvToggle = (value = process.env.PI_LOCAL_AGENTS_ONLY) => {
89
93
  return false;
90
94
  }
91
95
  };
92
- const getGlobalContextPaths = (agentDir = getAgentDir()) =>
93
- GLOBAL_CONTEXT_FILES.map((name) => join(agentDir, name)).filter((path) => existsSync(path));
94
- const getGlobalBlocks = (agentDir = getAgentDir()) =>
95
- getGlobalContextPaths(agentDir).map((path) => `## ${path}\n\n${readFileSync(path, "utf8")}\n\n`);
96
+ const getGlobalContextPaths = (agentDir = getAgentDir()) => GLOBAL_CONTEXT_FILES.map((name) => join(agentDir, name));
97
+ const getExistingGlobalContextPaths = (agentDir = getAgentDir()) =>
98
+ getGlobalContextPaths(agentDir).filter((path) => existsSync(path));
99
+ const getContextSectionEnd = (prompt, offset) => {
100
+ const candidates = [prompt.indexOf(SKILLS_HEADER, offset), prompt.indexOf(DATE_HEADER, offset)].filter(
101
+ (index) => index !== -1,
102
+ );
103
+ return candidates.length > 0 ? Math.min(...candidates) : prompt.length;
104
+ };
105
+ const getContextBlocks = (contextSection) => {
106
+ const matches = [...contextSection.matchAll(CONTEXT_BLOCK_HEADER)];
107
+ return matches.map((match, index) => ({
108
+ path: match[1],
109
+ start: match.index,
110
+ end: index + 1 < matches.length ? matches[index + 1].index : contextSection.length,
111
+ }));
112
+ };
113
+ const stripGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
114
+ const sectionStart = prompt.lastIndexOf(PROJECT_CONTEXT_HEADER);
115
+ if (sectionStart === -1) {
116
+ return { prompt, removedPaths: [] };
117
+ }
118
+ const contextStart = sectionStart + PROJECT_CONTEXT_HEADER.length;
119
+ const sectionEnd = getContextSectionEnd(prompt, contextStart);
120
+ const contextSection = prompt.slice(contextStart, sectionEnd);
121
+ const blocks = getContextBlocks(contextSection);
122
+ if (blocks.length === 0) {
123
+ return { prompt, removedPaths: [] };
124
+ }
125
+ const globalPathKeys = new Set(globalPaths.map(normalizePath));
126
+ const keptBlocks = [];
127
+ const removedPaths = [];
128
+ for (const block of blocks) {
129
+ const blockText = contextSection.slice(block.start, block.end);
130
+ if (globalPathKeys.has(normalizePath(block.path))) {
131
+ removedPaths.push(block.path);
132
+ } else {
133
+ keptBlocks.push(blockText);
134
+ }
135
+ }
136
+ if (removedPaths.length === 0) {
137
+ return { prompt, removedPaths: [] };
138
+ }
139
+ const prefix = prompt.slice(0, sectionStart);
140
+ const suffix = prompt.slice(sectionEnd);
141
+ if (keptBlocks.length === 0) {
142
+ return { prompt: `${prefix}${suffix}`, removedPaths: uniqueSorted(removedPaths) };
143
+ }
144
+ return {
145
+ prompt: `${prefix}${PROJECT_CONTEXT_HEADER}${keptBlocks.join("")}${suffix}`,
146
+ removedPaths: uniqueSorted(removedPaths),
147
+ };
148
+ };
96
149
  const getGitTopLevel = (start) => {
97
150
  const topLevel = runGit(start, ["rev-parse", "--show-toplevel"]);
98
151
  return topLevel ? normalizePath(topLevel) : undefined;
@@ -143,8 +196,7 @@ const clearMarkers = (state) => {
143
196
  rmSync(getMarkerPath(root), { force: true });
144
197
  }
145
198
  };
146
- const buildLocalOnlyNotice = (agentDir = getAgentDir()) => {
147
- const paths = getGlobalContextPaths(agentDir);
199
+ const buildLocalOnlyNotice = (paths = getExistingGlobalContextPaths(getAgentDir())) => {
148
200
  if (paths.length === 0) {
149
201
  return "";
150
202
  }
@@ -152,13 +204,15 @@ const buildLocalOnlyNotice = (agentDir = getAgentDir()) => {
152
204
  "# Local Context Mode",
153
205
  "This repo is in local-agents-only mode.",
154
206
  "Ignore instructions from these global context files even if they appear in older session messages, summaries, or retries:",
155
- ...paths.map((path) => `- ${path}`),
207
+ ...uniqueSorted(paths).map((path) => `- ${path}`),
156
208
  "Follow only repo-local AGENTS.md or CLAUDE.md guidance for this project.",
157
209
  ].join("\n");
158
210
  };
159
211
  const applyLocalOnlyPrompt = (prompt, agentDir = getAgentDir()) => {
160
- const stripped = stripGlobalBlocks(prompt, getGlobalBlocks(agentDir));
161
- const notice = buildLocalOnlyNotice(agentDir);
212
+ const { prompt: stripped, removedPaths } = stripGlobalContext(prompt, getGlobalContextPaths(agentDir));
213
+ const notice = buildLocalOnlyNotice(
214
+ removedPaths.length > 0 ? removedPaths : getExistingGlobalContextPaths(agentDir),
215
+ );
162
216
  return notice ? `${stripped}\n\n${notice}` : stripped;
163
217
  };
164
218
  const setStatus = (ctx) => {
@@ -168,6 +222,21 @@ const setStatus = (ctx) => {
168
222
  const mode = getMode(ctx.cwd);
169
223
  ctx.ui.setStatus(COMMAND, mode.enabled ? `AGENTS: local-only (${mode.source})` : undefined);
170
224
  };
225
+ const getProjectTarget = (state) =>
226
+ state.worktreeRoots.length > 1 ? `${state.projectRoot} and linked worktrees` : state.projectRoot;
227
+ const getOffNotification = (state) => {
228
+ const mode = getMode(state);
229
+ if (!mode.enabled) {
230
+ return `Disabled for ${getProjectTarget(state)}`;
231
+ }
232
+ if (mode.source === "global-config") {
233
+ return `Repo marker cleared for ${getProjectTarget(state)}, but local-agents-only is still enabled via global allowlist. Use /local-agents-only global-off to fully disable it.`;
234
+ }
235
+ if (mode.source === "env") {
236
+ return `Repo marker cleared for ${getProjectTarget(state)}, but local-agents-only is still enabled via PI_LOCAL_AGENTS_ONLY.`;
237
+ }
238
+ return `Repo marker cleared for ${getProjectTarget(state)}, but local-agents-only is still enabled via ${mode.source}.`;
239
+ };
171
240
 
172
241
  export function findProjectRoot(start = process.cwd()) {
173
242
  return getProjectState(start).projectRoot;
@@ -192,8 +261,8 @@ export function getMode(start = process.cwd(), envValue = process.env.PI_LOCAL_A
192
261
  return { enabled: false, source: "default" };
193
262
  }
194
263
 
195
- export function stripGlobalBlocks(prompt, blocks = getGlobalBlocks()) {
196
- return blocks.reduce((nextPrompt, block) => nextPrompt.replace(block, ""), prompt);
264
+ export function stripGlobalBlocks(prompt, globalPaths = getGlobalContextPaths()) {
265
+ return stripGlobalContext(prompt, globalPaths).prompt;
197
266
  }
198
267
 
199
268
  export default function localAgentsOnly(pi) {
@@ -210,7 +279,7 @@ export default function localAgentsOnly(pi) {
210
279
  case "off":
211
280
  clearMarkers(state);
212
281
  setStatus(ctx);
213
- ctx.ui.notify(`Disabled for ${state.projectRoot}${state.worktreeRoots.length > 1 ? ` and linked worktrees` : ""}`, "info");
282
+ ctx.ui.notify(getOffNotification(state), "info");
214
283
  return;
215
284
  case "global-on": {
216
285
  const config = readConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-local-agents-only",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Pi extension that strips global AGENTS.md and CLAUDE.md from the effective prompt for selected projects.",
5
5
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  "url": "https://github.com/fitchmultz/pi-local-agents-only/issues"
15
15
  },
16
16
  "homepage": "https://github.com/fitchmultz/pi-local-agents-only#readme",
17
- "files": ["extensions", "README.md", "LICENSE"],
17
+ "files": ["extensions", "README.md", "CHANGELOG.md", "LICENSE"],
18
18
  "pi": {
19
19
  "extensions": ["./extensions"]
20
20
  },