pi-local-agents-only 0.1.17 → 0.1.19

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 CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.19 - 2026-06-04
6
+
7
+ - updated the local pi development baseline to `@earendil-works/pi-coding-agent` `0.78.1` and regenerated the npm lockfile
8
+ - aligned prompt rewriting with pi `0.78.1` system-prompt metadata by using `before_agent_start` `systemPromptOptions` for loaded global context files
9
+ - removed legacy Markdown context stripping now that current pi prompt context uses XML boundaries
10
+ - refreshed CI to test supported Node.js release lines only
11
+
12
+ ## 0.1.18 - 2026-05-28
13
+
14
+ - updated the local pi development baseline to `@earendil-works/pi-coding-agent` `0.77.0` and regenerated the npm lockfile
15
+ - kept pi runtime packages as optional wildcard peers and removed the Node.js engine upper bound so future pi releases are not blocked at install time
16
+ - reviewed the pi `0.77.0` changelog and package guidance; the extension remains compatible with current system-prompt and package-loading behavior
17
+
5
18
  ## 0.1.17 - 2026-05-27
6
19
 
7
20
  - updated the local pi development baseline to `@earendil-works/pi-coding-agent` `0.76.0` and regenerated the npm lockfile
package/README.md CHANGED
@@ -16,6 +16,8 @@ Or install it directly from GitHub with pi:
16
16
  pi install https://github.com/fitchmultz/pi-local-agents-only
17
17
  ```
18
18
 
19
+ Compatibility note: this package is currently tested against pi `0.78.1` as the suggested floor, and pi-bundled runtime packages are declared as optional wildcard peers. That keeps installs forward-open for future pi releases: npm peer ranges should not block users from trying a newer pi, though runtime behavior is only verified against the tested baseline until a follow-up package release confirms it.
20
+
19
21
  ## Use
20
22
 
21
23
  Enable for the current repo:
@@ -5,7 +5,7 @@
5
5
  * Responsibilities: Detect repo and worktree opt-in state, manage repo and global toggles, add a local-only guardrail, and remove matching global context blocks before model calls.
6
6
  * Scope: Works as a pi extension package. It changes only the prompt the model sees, not pi's startup header.
7
7
  * Usage: Install the package, then use `/local-agents-only on|off|status|global-on|global-off`.
8
- * Invariants/Assumptions: pi injects context files as XML `<project_instructions path="...">` blocks in current releases and previously used Markdown `## /absolute/path` blocks; git worktrees that share a common git dir should share local-agents-only state.
8
+ * Invariants/Assumptions: pi injects context files as XML `<project_instructions path="...">` blocks in current releases; git worktrees that share a common git dir should share local-agents-only state.
9
9
  */
10
10
 
11
11
  import { execFileSync } from "node:child_process";
@@ -15,10 +15,10 @@ import { dirname, join, resolve } from "node:path";
15
15
 
16
16
  /** @typedef {import("@earendil-works/pi-coding-agent").ExtensionAPI} ExtensionAPI */
17
17
  /** @typedef {import("@earendil-works/pi-coding-agent").ExtensionContext} ExtensionContext */
18
+ /** @typedef {import("@earendil-works/pi-coding-agent").BuildSystemPromptOptions} BuildSystemPromptOptions */
18
19
  /** @typedef {{ projects: string[]; repositories: string[] }} LocalAgentsOnlyConfig */
19
20
  /** @typedef {{ start: string; projectRoot: string; repoId: string; worktreeRoots: string[] }} ProjectState */
20
21
  /** @typedef {{ enabled: boolean; source: "env" | "marker" | "global-config" | "default" }} Mode */
21
- /** @typedef {{ path: string; start: number; end: number }} ContextBlock */
22
22
  /** @typedef {{ prompt: string; removedPaths: string[] }} StripResult */
23
23
 
24
24
  class ConfigError extends Error {
@@ -37,13 +37,9 @@ const MARKER = join(".pi", COMMAND);
37
37
  const GLOBAL_CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md"];
38
38
  const ENV_TRUE = ["1", "true", "yes", "on"];
39
39
  const ENV_FALSE = ["0", "false", "no", "off"];
40
- const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
41
40
  const PROJECT_CONTEXT_XML_START = "<project_context>";
42
41
  const PROJECT_CONTEXT_XML_END = "</project_context>";
43
42
  const PROJECT_CONTEXT_XML_PREFIX = "<project_context>\n\nProject-specific instructions and guidelines:\n\n";
44
- const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
45
- const DATE_HEADER = "\nCurrent date:";
46
- const CONTEXT_BLOCK_HEADER = /^## ([^\n]+(?:AGENTS|CLAUDE)\.md)\n\n/gm;
47
43
  const CONTEXT_XML_BLOCK = /<project_instructions path="([^"]+(?:AGENTS|CLAUDE)\.md)">\n[\s\S]*?<\/project_instructions>\n*/g;
48
44
  const emptyConfig = () => ({ projects: [], repositories: [] });
49
45
 
@@ -257,28 +253,17 @@ const getExistingGlobalContextPaths = (agentDir = getAgentDir()) =>
257
253
  getGlobalContextPaths(agentDir).filter((path) => existsSync(path));
258
254
 
259
255
  /**
260
- * @param {string} prompt
261
- * @param {number} offset
262
- * @returns {number}
256
+ * @param {BuildSystemPromptOptions | undefined} options
257
+ * @param {string} [agentDir]
258
+ * @returns {string[]}
263
259
  */
264
- const getContextSectionEnd = (prompt, offset) => {
265
- const candidates = [prompt.indexOf(SKILLS_HEADER, offset), prompt.indexOf(DATE_HEADER, offset)].filter(
266
- (index) => index !== -1,
260
+ const getLoadedGlobalContextPaths = (options, agentDir = getAgentDir()) => {
261
+ const globalPathKeys = new Set(getGlobalContextPaths(agentDir).map(normalizePath));
262
+ return uniqueSorted(
263
+ (options?.contextFiles ?? [])
264
+ .map((file) => file.path)
265
+ .filter((path) => globalPathKeys.has(normalizePath(path))),
267
266
  );
268
- return candidates.length > 0 ? Math.min(...candidates) : prompt.length;
269
- };
270
-
271
- /**
272
- * @param {string} contextSection
273
- * @returns {ContextBlock[]}
274
- */
275
- const getContextBlocks = (contextSection) => {
276
- const matches = [...contextSection.matchAll(CONTEXT_BLOCK_HEADER)];
277
- return matches.map((match, index) => ({
278
- path: match[1],
279
- start: match.index ?? 0,
280
- end: index + 1 < matches.length ? (matches[index + 1].index ?? contextSection.length) : contextSection.length,
281
- }));
282
267
  };
283
268
 
284
269
  /**
@@ -286,7 +271,7 @@ const getContextBlocks = (contextSection) => {
286
271
  * @param {string[]} [globalPaths]
287
272
  * @returns {StripResult}
288
273
  */
289
- const stripXmlGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
274
+ const stripGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
290
275
  const contextTagStart = prompt.lastIndexOf(PROJECT_CONTEXT_XML_START);
291
276
  if (contextTagStart === -1) {
292
277
  return { prompt, removedPaths: [] };
@@ -312,7 +297,7 @@ const stripXmlGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) =>
312
297
  if (globalPathKeys.has(normalizePath(path))) {
313
298
  removedPaths.push(path);
314
299
  } else {
315
- keptBlocks.push(blockText.endsWith("\n\n") ? blockText : `${blockText}\n`);
300
+ keptBlocks.push(blockText.trimEnd());
316
301
  }
317
302
  }
318
303
  if (removedPaths.length === 0) {
@@ -321,68 +306,14 @@ const stripXmlGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) =>
321
306
  const prefix = prompt.slice(0, contextTagStart).replace(/\n\n$/u, "\n");
322
307
  const suffix = prompt.slice(sectionEnd);
323
308
  if (keptBlocks.length === 0) {
324
- return { prompt: `${prefix}${suffix}`, removedPaths: uniqueSorted(removedPaths) };
309
+ return { prompt: `${prefix}${suffix.replace(/^\n/u, "")}`, removedPaths: uniqueSorted(removedPaths) };
325
310
  }
326
311
  return {
327
- prompt: `${prefix}${PROJECT_CONTEXT_XML_PREFIX}${keptBlocks.join("\n")}${PROJECT_CONTEXT_XML_END}${suffix}`,
312
+ prompt: `${prefix}${PROJECT_CONTEXT_XML_PREFIX}${keptBlocks.join("\n\n")}\n${PROJECT_CONTEXT_XML_END}${suffix}`,
328
313
  removedPaths: uniqueSorted(removedPaths),
329
314
  };
330
315
  };
331
316
 
332
- /**
333
- * @param {string} prompt
334
- * @param {string[]} [globalPaths]
335
- * @returns {StripResult}
336
- */
337
- const stripMarkdownGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
338
- const sectionStart = prompt.lastIndexOf(PROJECT_CONTEXT_HEADER);
339
- if (sectionStart === -1) {
340
- return { prompt, removedPaths: [] };
341
- }
342
- const contextStart = sectionStart + PROJECT_CONTEXT_HEADER.length;
343
- const sectionEnd = getContextSectionEnd(prompt, contextStart);
344
- const contextSection = prompt.slice(contextStart, sectionEnd);
345
- const blocks = getContextBlocks(contextSection);
346
- if (blocks.length === 0) {
347
- return { prompt, removedPaths: [] };
348
- }
349
- const globalPathKeys = new Set(globalPaths.map(normalizePath));
350
- /** @type {string[]} */
351
- const keptBlocks = [];
352
- /** @type {string[]} */
353
- const removedPaths = [];
354
- for (const block of blocks) {
355
- const blockText = contextSection.slice(block.start, block.end);
356
- if (globalPathKeys.has(normalizePath(block.path))) {
357
- removedPaths.push(block.path);
358
- } else {
359
- keptBlocks.push(blockText);
360
- }
361
- }
362
- if (removedPaths.length === 0) {
363
- return { prompt, removedPaths: [] };
364
- }
365
- const prefix = prompt.slice(0, sectionStart);
366
- const suffix = prompt.slice(sectionEnd);
367
- if (keptBlocks.length === 0) {
368
- return { prompt: `${prefix}${suffix}`, removedPaths: uniqueSorted(removedPaths) };
369
- }
370
- return {
371
- prompt: `${prefix}${PROJECT_CONTEXT_HEADER}${keptBlocks.join("")}${suffix}`,
372
- removedPaths: uniqueSorted(removedPaths),
373
- };
374
- };
375
-
376
- /**
377
- * @param {string} prompt
378
- * @param {string[]} [globalPaths]
379
- * @returns {StripResult}
380
- */
381
- const stripGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
382
- const xmlResult = stripXmlGlobalContext(prompt, globalPaths);
383
- return xmlResult.removedPaths.length > 0 ? xmlResult : stripMarkdownGlobalContext(prompt, globalPaths);
384
- };
385
-
386
317
  /**
387
318
  * @param {string} start
388
319
  * @returns {string | undefined}
@@ -484,12 +415,15 @@ const buildLocalOnlyNotice = (paths = getExistingGlobalContextPaths(getAgentDir(
484
415
  /**
485
416
  * @param {string} prompt
486
417
  * @param {string} [agentDir]
418
+ * @param {BuildSystemPromptOptions} [systemPromptOptions]
487
419
  * @returns {string}
488
420
  */
489
- const applyLocalOnlyPrompt = (prompt, agentDir = getAgentDir()) => {
490
- const { prompt: stripped, removedPaths } = stripGlobalContext(prompt, getGlobalContextPaths(agentDir));
421
+ const applyLocalOnlyPrompt = (prompt, agentDir = getAgentDir(), systemPromptOptions = undefined) => {
422
+ const loadedGlobalPaths = getLoadedGlobalContextPaths(systemPromptOptions, agentDir);
423
+ const globalPaths = loadedGlobalPaths.length > 0 ? loadedGlobalPaths : getGlobalContextPaths(agentDir);
424
+ const { prompt: stripped, removedPaths } = stripGlobalContext(prompt, globalPaths);
491
425
  const notice = buildLocalOnlyNotice(
492
- removedPaths.length > 0 ? removedPaths : getExistingGlobalContextPaths(agentDir),
426
+ removedPaths.length > 0 ? removedPaths : loadedGlobalPaths.length > 0 ? loadedGlobalPaths : getExistingGlobalContextPaths(agentDir),
493
427
  );
494
428
  return notice ? `${stripped}\n\n${notice}` : stripped;
495
429
  };
@@ -666,6 +600,8 @@ export default function localAgentsOnly(pi) {
666
600
  });
667
601
 
668
602
  pi.on("before_agent_start", (event, ctx) => {
669
- return getMode(ctx.cwd).enabled ? { systemPrompt: applyLocalOnlyPrompt(event.systemPrompt) } : undefined;
603
+ return getMode(ctx.cwd).enabled
604
+ ? { systemPrompt: applyLocalOnlyPrompt(event.systemPrompt, getAgentDir(), event.systemPromptOptions) }
605
+ : undefined;
670
606
  });
671
607
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-local-agents-only",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
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",
@@ -37,15 +37,20 @@
37
37
  "prepublishOnly": "npm run check"
38
38
  },
39
39
  "devDependencies": {
40
- "@earendil-works/pi-coding-agent": "^0.76.0",
40
+ "@earendil-works/pi-coding-agent": "^0.78.1",
41
41
  "@types/node": "^25.9.1",
42
42
  "typescript": "^6.0.3"
43
43
  },
44
- "packageManager": "npm@11.0.0",
44
+ "packageManager": "npm@11.16.0",
45
45
  "engines": {
46
- "node": ">=22 <25"
46
+ "node": ">=22.19.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@earendil-works/pi-coding-agent": "*"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "@earendil-works/pi-coding-agent": {
53
+ "optional": true
54
+ }
50
55
  }
51
56
  }