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 +51 -0
- package/extensions/local-agents-only.js +150 -0
- package/package.json +13 -0
- package/test/local-agents-only.test.mjs +50 -0
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
|
+
});
|