pi-local-agents-only 0.1.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/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # pi-local-agents-only
2
+
3
+ Use repo-local `AGENTS.md` only for selected projects by stripping global `AGENTS.md` and `CLAUDE.md` from pi's effective prompt.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install git:github.com/fitchmultz/pi-local-agents-only
9
+ ```
10
+
11
+ ## Use
12
+
13
+ Enable for the current repo:
14
+
15
+ ```bash
16
+ /local-agents-only on
17
+ ```
18
+
19
+ Disable for the current repo:
20
+
21
+ ```bash
22
+ /local-agents-only off
23
+ ```
24
+
25
+ Enable or disable via the global allowlist:
26
+
27
+ ```bash
28
+ /local-agents-only global-on
29
+ /local-agents-only global-off
30
+ ```
31
+
32
+ Check status:
33
+
34
+ ```bash
35
+ /local-agents-only status
36
+ ```
37
+
38
+ Repo opt-in uses this marker file:
39
+
40
+ ```text
41
+ .pi/local-agents-only
42
+ ```
43
+
44
+ Env override for one run:
45
+
46
+ ```bash
47
+ PI_LOCAL_AGENTS_ONLY=1 pi
48
+ PI_LOCAL_AGENTS_ONLY=0 pi
49
+ ```
50
+
51
+ This changes the prompt the model sees. It does not change pi's startup header.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Purpose: Strip pi's global AGENTS.md and CLAUDE.md blocks from the effective prompt for opted-in projects.
3
+ * Responsibilities: Detect repo opt-in state, manage repo and global toggles, and remove matching global context blocks before model calls.
4
+ * Scope: Works as a pi extension package. It changes only the prompt the model sees, not pi's startup header.
5
+ * Usage: Install the package, then use `/local-agents-only on|off|status|global-on|global-off`.
6
+ * Invariants/Assumptions: pi injects context files as `## /absolute/path\n\n<file contents>\n\n`.
7
+ */
8
+
9
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join, resolve } from "node:path";
12
+
13
+ const COMMAND = "local-agents-only";
14
+ const MARKER = join(".pi", COMMAND);
15
+ const GLOBAL_CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md"];
16
+ const ENV_TRUE = ["1", "true", "yes", "on"];
17
+ const ENV_FALSE = ["0", "false", "no", "off"];
18
+
19
+ const getAgentDir = () => {
20
+ const env = process.env.PI_CODING_AGENT_DIR;
21
+ if (env === "~") {
22
+ return homedir();
23
+ }
24
+ if (env?.startsWith("~/")) {
25
+ return join(homedir(), env.slice(2));
26
+ }
27
+ return env || join(homedir(), ".pi", "agent");
28
+ };
29
+ const normalizePath = (path) => resolve(path).replace(/\\/g, "/");
30
+ const CONFIG = () => join(getAgentDir(), `${COMMAND}.json`);
31
+ const getMarkerPath = (projectRoot) => join(projectRoot, MARKER);
32
+ const readProjects = (configPath = CONFIG()) => {
33
+ try {
34
+ const { projects = [] } = JSON.parse(readFileSync(configPath, "utf8"));
35
+ return Array.isArray(projects) ? projects.map(normalizePath) : [];
36
+ } catch {
37
+ return [];
38
+ }
39
+ };
40
+ const writeProjects = (projects, configPath = CONFIG()) => {
41
+ mkdirSync(dirname(configPath), { recursive: true });
42
+ writeFileSync(
43
+ configPath,
44
+ JSON.stringify({ projects: [...new Set(projects.map(normalizePath))].sort() }, null, 2) + "\n",
45
+ );
46
+ };
47
+ const getEnvToggle = (value = process.env.PI_LOCAL_AGENTS_ONLY) => {
48
+ const toggle = `${value ?? ""}`.trim().toLowerCase();
49
+ if (ENV_TRUE.includes(toggle)) {
50
+ return true;
51
+ }
52
+ if (ENV_FALSE.includes(toggle)) {
53
+ return false;
54
+ }
55
+ };
56
+ const getGlobalBlocks = (agentDir = getAgentDir()) =>
57
+ GLOBAL_CONTEXT_FILES.flatMap((name) => {
58
+ const path = join(agentDir, name);
59
+ return existsSync(path) ? [`## ${path}\n\n${readFileSync(path, "utf8")}\n\n`] : [];
60
+ });
61
+ const setStatus = (ctx) => {
62
+ if (!ctx.hasUI) {
63
+ return;
64
+ }
65
+ const mode = getMode(findProjectRoot(ctx.cwd));
66
+ ctx.ui.setStatus(COMMAND, mode.enabled ? `AGENTS: local-only (${mode.source})` : undefined);
67
+ };
68
+
69
+ export function findProjectRoot(start = process.cwd()) {
70
+ let current = resolve(start);
71
+ while (!existsSync(join(current, ".git"))) {
72
+ const parent = dirname(current);
73
+ if (parent === current) {
74
+ break;
75
+ }
76
+ current = parent;
77
+ }
78
+ return current;
79
+ }
80
+
81
+ export function getMode(projectRoot, envValue = process.env.PI_LOCAL_AGENTS_ONLY, configPath = CONFIG()) {
82
+ const envToggle = getEnvToggle(envValue);
83
+ if (envToggle !== undefined) {
84
+ return { enabled: envToggle, source: "env" };
85
+ }
86
+ if (existsSync(getMarkerPath(projectRoot))) {
87
+ return { enabled: true, source: "marker" };
88
+ }
89
+ if (readProjects(configPath).includes(normalizePath(projectRoot))) {
90
+ return { enabled: true, source: "global-config" };
91
+ }
92
+ return { enabled: false, source: "default" };
93
+ }
94
+
95
+ export function stripGlobalBlocks(prompt, blocks = getGlobalBlocks()) {
96
+ return blocks.reduce((nextPrompt, block) => nextPrompt.replace(block, ""), prompt);
97
+ }
98
+
99
+ export default function localAgentsOnly(pi) {
100
+ pi.registerCommand(COMMAND, {
101
+ description: "Use only repo-local AGENTS prompt context",
102
+ handler: async (args, ctx) => {
103
+ const projectRoot = findProjectRoot(ctx.cwd);
104
+ switch ((args.trim() || "status").toLowerCase()) {
105
+ case "on":
106
+ mkdirSync(dirname(getMarkerPath(projectRoot)), { recursive: true });
107
+ writeFileSync(getMarkerPath(projectRoot), "\n");
108
+ setStatus(ctx);
109
+ ctx.ui.notify(`Enabled at ${getMarkerPath(projectRoot)}`, "info");
110
+ return;
111
+ case "off":
112
+ rmSync(getMarkerPath(projectRoot), { force: true });
113
+ setStatus(ctx);
114
+ ctx.ui.notify("Disabled for this repo", "info");
115
+ return;
116
+ case "global-on":
117
+ writeProjects([...readProjects(), projectRoot]);
118
+ setStatus(ctx);
119
+ ctx.ui.notify(`Global allowlist enabled for ${normalizePath(projectRoot)}`, "info");
120
+ return;
121
+ case "global-off":
122
+ writeProjects(readProjects().filter((path) => path !== normalizePath(projectRoot)));
123
+ setStatus(ctx);
124
+ ctx.ui.notify(`Global allowlist disabled for ${normalizePath(projectRoot)}`, "info");
125
+ return;
126
+ case "status": {
127
+ const mode = getMode(projectRoot);
128
+ ctx.ui.notify(`local-agents-only: ${mode.enabled ? `enabled via ${mode.source}` : "disabled"}`, "info");
129
+ return;
130
+ }
131
+ default:
132
+ ctx.ui.notify("Usage: /local-agents-only [status|on|off|global-on|global-off]", "warning");
133
+ }
134
+ },
135
+ });
136
+
137
+ pi.on("session_start", (_event, ctx) => {
138
+ setStatus(ctx);
139
+ });
140
+
141
+ pi.on("session_shutdown", (_event, ctx) => {
142
+ if (ctx.hasUI) {
143
+ ctx.ui.setStatus(COMMAND, undefined);
144
+ }
145
+ });
146
+
147
+ pi.on("before_agent_start", (event, ctx) => {
148
+ return getMode(findProjectRoot(ctx.cwd)).enabled ? { systemPrompt: stripGlobalBlocks(event.systemPrompt) } : undefined;
149
+ });
150
+ }
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "pi-local-agents-only",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension that strips global AGENTS.md and CLAUDE.md from the effective prompt for selected projects.",
5
+ "type": "module",
6
+ "keywords": ["pi-package"],
7
+ "pi": {
8
+ "extensions": ["./extensions"]
9
+ },
10
+ "scripts": {
11
+ "test": "node --test"
12
+ }
13
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Purpose: Verify the local-agents-only extension's repo detection, activation precedence, and prompt stripping.
3
+ * Responsibilities: Test git-root discovery, env and config precedence, and removal of global prompt blocks.
4
+ * Scope: Minimal unit tests for the extension's exported helpers.
5
+ * Usage: Run `npm test` from the package root.
6
+ * Invariants/Assumptions: Tests use temporary directories and do not touch the user's real pi config.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ import { findProjectRoot, getMode, stripGlobalBlocks } from "../extensions/local-agents-only.js";
16
+
17
+ test("findProjectRoot returns the nearest git root", () => {
18
+ const root = mkdtempSync(join(tmpdir(), "pi-local-agents-only-root-"));
19
+ const nested = join(root, "a", "b");
20
+ mkdirSync(join(root, ".git"), { recursive: true });
21
+ mkdirSync(nested, { recursive: true });
22
+ assert.equal(findProjectRoot(nested), root);
23
+ });
24
+
25
+ test("getMode prefers env override, then repo marker, then global config", () => {
26
+ const root = mkdtempSync(join(tmpdir(), "pi-local-agents-only-mode-"));
27
+ const configPath = join(root, "local-agents-only.json");
28
+ const markerPath = join(root, ".pi", "local-agents-only");
29
+ mkdirSync(join(root, ".pi"), { recursive: true });
30
+ writeFileSync(configPath, JSON.stringify({ projects: [root] }));
31
+ writeFileSync(markerPath, "\n");
32
+ assert.deepEqual(getMode(root, "1", configPath), { enabled: true, source: "env" });
33
+ assert.deepEqual(getMode(root, "0", configPath), { enabled: false, source: "env" });
34
+ assert.deepEqual(getMode(root, "", configPath), { enabled: true, source: "marker" });
35
+ assert.equal(rmSync(markerPath, { force: true }), undefined);
36
+ assert.deepEqual(getMode(root, "", configPath), { enabled: true, source: "global-config" });
37
+ assert.deepEqual(getMode(mkdtempSync(join(tmpdir(), "pi-local-agents-only-default-")), "", configPath), {
38
+ enabled: false,
39
+ source: "default",
40
+ });
41
+ });
42
+
43
+ test("stripGlobalBlocks removes global blocks and keeps local AGENTS or CLAUDE context", () => {
44
+ const globalAgents = "## /home/me/.pi/agent/AGENTS.md\n\nA\n\n";
45
+ const globalClaude = "## /home/me/.pi/agent/CLAUDE.md\n\nB\n\n";
46
+ const localAgents = "## /repo/AGENTS.md\n\nLOCAL AGENTS\n\n";
47
+ const localClaude = "## /repo/subdir/CLAUDE.md\n\nLOCAL CLAUDE\n\n";
48
+ const prompt = `${globalAgents}${globalClaude}${localAgents}${localClaude}`;
49
+ assert.equal(stripGlobalBlocks(prompt, [globalAgents, globalClaude]), `${localAgents}${localClaude}`);
50
+ });