pi-local-agents-only 0.1.18 → 0.1.20
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 +12 -0
- package/README.md +1 -1
- package/extensions/local-agents-only.js +35 -92
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.20 - 2026-06-15
|
|
6
|
+
|
|
7
|
+
- updated the local pi development baseline to `@earendil-works/pi-coding-agent` `0.79.4` and refreshed the npm lockfile
|
|
8
|
+
- validated project-trust no-UI/global-block stripping behavior and package load under pi `0.79.4`
|
|
9
|
+
|
|
10
|
+
## 0.1.19 - 2026-06-04
|
|
11
|
+
|
|
12
|
+
- updated the local pi development baseline to `@earendil-works/pi-coding-agent` `0.78.1` and regenerated the npm lockfile
|
|
13
|
+
- aligned prompt rewriting with pi `0.78.1` system-prompt metadata by using `before_agent_start` `systemPromptOptions` for loaded global context files
|
|
14
|
+
- removed legacy Markdown context stripping now that current pi prompt context uses XML boundaries
|
|
15
|
+
- refreshed CI to test supported Node.js release lines only
|
|
16
|
+
|
|
5
17
|
## 0.1.18 - 2026-05-28
|
|
6
18
|
|
|
7
19
|
- updated the local pi development baseline to `@earendil-works/pi-coding-agent` `0.77.0` and regenerated the npm lockfile
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ 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 tested against
|
|
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
20
|
|
|
21
21
|
## Use
|
|
22
22
|
|
|
@@ -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
|
|
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 {
|
|
261
|
-
* @param {
|
|
262
|
-
* @returns {
|
|
256
|
+
* @param {BuildSystemPromptOptions | undefined} options
|
|
257
|
+
* @param {string} [agentDir]
|
|
258
|
+
* @returns {string[]}
|
|
263
259
|
*/
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
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
|
|
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.
|
|
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,22 +415,31 @@ 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
|
|
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
|
};
|
|
496
430
|
|
|
431
|
+
/** @param {ExtensionContext} ctx */
|
|
432
|
+
const isProjectTrusted = (ctx) => typeof ctx.isProjectTrusted === "function" ? ctx.isProjectTrusted() : true;
|
|
433
|
+
|
|
434
|
+
/** @param {ExtensionContext} ctx */
|
|
435
|
+
const getTrustedMode = (ctx) => getMode(ctx.cwd, undefined, undefined, { projectTrusted: isProjectTrusted(ctx) });
|
|
436
|
+
|
|
497
437
|
/** @param {ExtensionContext} ctx */
|
|
498
438
|
const setStatus = (ctx) => {
|
|
499
439
|
if (!ctx.hasUI) {
|
|
500
440
|
return;
|
|
501
441
|
}
|
|
502
|
-
const mode =
|
|
442
|
+
const mode = getTrustedMode(ctx);
|
|
503
443
|
ctx.ui.setStatus(COMMAND, mode.enabled ? `AGENTS: local-only (${mode.source})` : undefined);
|
|
504
444
|
};
|
|
505
445
|
|
|
@@ -563,15 +503,16 @@ export function findProjectRoot(start = process.cwd()) {
|
|
|
563
503
|
* @param {string | ProjectState} [start]
|
|
564
504
|
* @param {string | undefined} [envValue]
|
|
565
505
|
* @param {string} [configPath]
|
|
506
|
+
* @param {{ projectTrusted?: boolean }} [options]
|
|
566
507
|
* @returns {Mode}
|
|
567
508
|
*/
|
|
568
|
-
export function getMode(start = process.cwd(), envValue = process.env.PI_LOCAL_AGENTS_ONLY, configPath = CONFIG()) {
|
|
509
|
+
export function getMode(start = process.cwd(), envValue = process.env.PI_LOCAL_AGENTS_ONLY, configPath = CONFIG(), options = {}) {
|
|
569
510
|
const state = typeof start === "string" ? getProjectState(start) : start;
|
|
570
511
|
const envToggle = getEnvToggle(envValue);
|
|
571
512
|
if (envToggle !== undefined) {
|
|
572
513
|
return { enabled: envToggle, source: "env" };
|
|
573
514
|
}
|
|
574
|
-
if (hasMarker(state)) {
|
|
515
|
+
if (options.projectTrusted !== false && hasMarker(state)) {
|
|
575
516
|
return { enabled: true, source: "marker" };
|
|
576
517
|
}
|
|
577
518
|
const { projects, repositories } = readConfig(configPath);
|
|
@@ -642,7 +583,7 @@ export default function localAgentsOnly(pi) {
|
|
|
642
583
|
return;
|
|
643
584
|
}
|
|
644
585
|
case "status": {
|
|
645
|
-
const mode = getMode(state);
|
|
586
|
+
const mode = getMode(state, undefined, undefined, { projectTrusted: isProjectTrusted(ctx) });
|
|
646
587
|
ctx.ui.notify(
|
|
647
588
|
`local-agents-only: ${mode.enabled ? `enabled via ${mode.source}` : "disabled"} (${state.projectRoot})`,
|
|
648
589
|
"info",
|
|
@@ -666,6 +607,8 @@ export default function localAgentsOnly(pi) {
|
|
|
666
607
|
});
|
|
667
608
|
|
|
668
609
|
pi.on("before_agent_start", (event, ctx) => {
|
|
669
|
-
return
|
|
610
|
+
return getTrustedMode(ctx).enabled
|
|
611
|
+
? { systemPrompt: applyLocalOnlyPrompt(event.systemPrompt, getAgentDir(), event.systemPromptOptions) }
|
|
612
|
+
: undefined;
|
|
670
613
|
});
|
|
671
614
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-local-agents-only",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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,7 +37,7 @@
|
|
|
37
37
|
"prepublishOnly": "npm run check"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@earendil-works/pi-coding-agent": "
|
|
40
|
+
"@earendil-works/pi-coding-agent": "0.79.4",
|
|
41
41
|
"@types/node": "^25.9.1",
|
|
42
42
|
"typescript": "^6.0.3"
|
|
43
43
|
},
|