pi-local-agents-only 0.1.5 → 0.1.6
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 +7 -0
- package/extensions/local-agents-only.js +151 -8
- package/package.json +24 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6 - 2026-04-07
|
|
4
|
+
|
|
5
|
+
- add dev-time static checking with `tsc --noEmit`
|
|
6
|
+
- add a local `check` script that runs tests plus typechecking
|
|
7
|
+
- keep pi core typing support in `devDependencies` only so this stays a lightweight published package
|
|
8
|
+
- annotate the extension with `// @ts-check` and JSDoc types for earlier local error detection
|
|
9
|
+
|
|
3
10
|
## 0.1.5 - 2026-04-07
|
|
4
11
|
|
|
5
12
|
- strip already-loaded global `AGENTS.md` / `CLAUDE.md` context reliably instead of rereading live files from disk
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Purpose: Strip pi's global AGENTS.md and CLAUDE.md blocks from the effective prompt for opted-in projects.
|
|
3
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.
|
|
@@ -11,6 +13,14 @@ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node
|
|
|
11
13
|
import { homedir } from "node:os";
|
|
12
14
|
import { dirname, join, resolve } from "node:path";
|
|
13
15
|
|
|
16
|
+
/** @typedef {import("@mariozechner/pi-coding-agent").ExtensionAPI} ExtensionAPI */
|
|
17
|
+
/** @typedef {import("@mariozechner/pi-coding-agent").ExtensionContext} ExtensionContext */
|
|
18
|
+
/** @typedef {{ projects: string[]; repositories: string[] }} LocalAgentsOnlyConfig */
|
|
19
|
+
/** @typedef {{ start: string; projectRoot: string; repoId: string; worktreeRoots: string[] }} ProjectState */
|
|
20
|
+
/** @typedef {{ enabled: boolean; source: "env" | "marker" | "global-config" | "default" }} Mode */
|
|
21
|
+
/** @typedef {{ path: string; start: number; end: number }} ContextBlock */
|
|
22
|
+
/** @typedef {{ prompt: string; removedPaths: string[] }} StripResult */
|
|
23
|
+
|
|
14
24
|
const COMMAND = "local-agents-only";
|
|
15
25
|
const MARKER = join(".pi", COMMAND);
|
|
16
26
|
const GLOBAL_CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
@@ -21,6 +31,7 @@ const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions
|
|
|
21
31
|
const DATE_HEADER = "\nCurrent date:";
|
|
22
32
|
const CONTEXT_BLOCK_HEADER = /^## ([^\n]+(?:AGENTS|CLAUDE)\.md)\n\n/gm;
|
|
23
33
|
|
|
34
|
+
/** @returns {string} */
|
|
24
35
|
const getAgentDir = () => {
|
|
25
36
|
const env = process.env.PI_CODING_AGENT_DIR;
|
|
26
37
|
if (env === "~") {
|
|
@@ -31,10 +42,24 @@ const getAgentDir = () => {
|
|
|
31
42
|
}
|
|
32
43
|
return env || join(homedir(), ".pi", "agent");
|
|
33
44
|
};
|
|
45
|
+
|
|
46
|
+
/** @param {string} path */
|
|
34
47
|
const normalizePath = (path) => resolve(path).replace(/\\/g, "/");
|
|
48
|
+
|
|
49
|
+
/** @returns {string} */
|
|
35
50
|
const CONFIG = () => join(getAgentDir(), `${COMMAND}.json`);
|
|
51
|
+
|
|
52
|
+
/** @param {string} projectRoot */
|
|
36
53
|
const getMarkerPath = (projectRoot) => join(projectRoot, MARKER);
|
|
54
|
+
|
|
55
|
+
/** @param {string[]} values */
|
|
37
56
|
const uniqueSorted = (values) => [...new Set(values.map(normalizePath))].sort();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} start
|
|
60
|
+
* @param {(dir: string) => boolean} predicate
|
|
61
|
+
* @returns {string | undefined}
|
|
62
|
+
*/
|
|
38
63
|
const walkUp = (start, predicate) => {
|
|
39
64
|
let current = resolve(start);
|
|
40
65
|
while (true) {
|
|
@@ -43,11 +68,17 @@ const walkUp = (start, predicate) => {
|
|
|
43
68
|
}
|
|
44
69
|
const parent = dirname(current);
|
|
45
70
|
if (parent === current) {
|
|
46
|
-
return;
|
|
71
|
+
return undefined;
|
|
47
72
|
}
|
|
48
73
|
current = parent;
|
|
49
74
|
}
|
|
50
75
|
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} start
|
|
79
|
+
* @param {string[]} args
|
|
80
|
+
* @returns {string | undefined}
|
|
81
|
+
*/
|
|
51
82
|
const runGit = (start, args) => {
|
|
52
83
|
try {
|
|
53
84
|
return execFileSync("git", args, {
|
|
@@ -56,20 +87,33 @@ const runGit = (start, args) => {
|
|
|
56
87
|
stdio: ["ignore", "pipe", "ignore"],
|
|
57
88
|
}).trim();
|
|
58
89
|
} catch {
|
|
59
|
-
return;
|
|
90
|
+
return undefined;
|
|
60
91
|
}
|
|
61
92
|
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} [configPath]
|
|
96
|
+
* @returns {LocalAgentsOnlyConfig}
|
|
97
|
+
*/
|
|
62
98
|
const readConfig = (configPath = CONFIG()) => {
|
|
63
99
|
try {
|
|
64
|
-
const { projects
|
|
100
|
+
const parsed = /** @type {{ projects?: unknown; repositories?: unknown }} */ (
|
|
101
|
+
JSON.parse(readFileSync(configPath, "utf8"))
|
|
102
|
+
);
|
|
103
|
+
const { projects = [], repositories = [] } = parsed;
|
|
65
104
|
return {
|
|
66
|
-
projects: Array.isArray(projects) ? projects.map(normalizePath) : [],
|
|
67
|
-
repositories: Array.isArray(repositories) ? repositories.map(normalizePath) : [],
|
|
105
|
+
projects: Array.isArray(projects) ? projects.map((value) => normalizePath(String(value))) : [],
|
|
106
|
+
repositories: Array.isArray(repositories) ? repositories.map((value) => normalizePath(String(value))) : [],
|
|
68
107
|
};
|
|
69
108
|
} catch {
|
|
70
109
|
return { projects: [], repositories: [] };
|
|
71
110
|
}
|
|
72
111
|
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {LocalAgentsOnlyConfig} config
|
|
115
|
+
* @param {string} [configPath]
|
|
116
|
+
*/
|
|
73
117
|
const writeConfig = ({ projects, repositories }, configPath = CONFIG()) => {
|
|
74
118
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
75
119
|
writeFileSync(
|
|
@@ -84,6 +128,11 @@ const writeConfig = ({ projects, repositories }, configPath = CONFIG()) => {
|
|
|
84
128
|
) + "\n",
|
|
85
129
|
);
|
|
86
130
|
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string | undefined} [value]
|
|
134
|
+
* @returns {boolean | undefined}
|
|
135
|
+
*/
|
|
87
136
|
const getEnvToggle = (value = process.env.PI_LOCAL_AGENTS_ONLY) => {
|
|
88
137
|
const toggle = `${value ?? ""}`.trim().toLowerCase();
|
|
89
138
|
if (ENV_TRUE.includes(toggle)) {
|
|
@@ -92,24 +141,46 @@ const getEnvToggle = (value = process.env.PI_LOCAL_AGENTS_ONLY) => {
|
|
|
92
141
|
if (ENV_FALSE.includes(toggle)) {
|
|
93
142
|
return false;
|
|
94
143
|
}
|
|
144
|
+
return undefined;
|
|
95
145
|
};
|
|
146
|
+
|
|
147
|
+
/** @param {string} [agentDir] */
|
|
96
148
|
const getGlobalContextPaths = (agentDir = getAgentDir()) => GLOBAL_CONTEXT_FILES.map((name) => join(agentDir, name));
|
|
149
|
+
|
|
150
|
+
/** @param {string} [agentDir] */
|
|
97
151
|
const getExistingGlobalContextPaths = (agentDir = getAgentDir()) =>
|
|
98
152
|
getGlobalContextPaths(agentDir).filter((path) => existsSync(path));
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {string} prompt
|
|
156
|
+
* @param {number} offset
|
|
157
|
+
* @returns {number}
|
|
158
|
+
*/
|
|
99
159
|
const getContextSectionEnd = (prompt, offset) => {
|
|
100
160
|
const candidates = [prompt.indexOf(SKILLS_HEADER, offset), prompt.indexOf(DATE_HEADER, offset)].filter(
|
|
101
161
|
(index) => index !== -1,
|
|
102
162
|
);
|
|
103
163
|
return candidates.length > 0 ? Math.min(...candidates) : prompt.length;
|
|
104
164
|
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {string} contextSection
|
|
168
|
+
* @returns {ContextBlock[]}
|
|
169
|
+
*/
|
|
105
170
|
const getContextBlocks = (contextSection) => {
|
|
106
171
|
const matches = [...contextSection.matchAll(CONTEXT_BLOCK_HEADER)];
|
|
107
172
|
return matches.map((match, index) => ({
|
|
108
173
|
path: match[1],
|
|
109
|
-
start: match.index,
|
|
110
|
-
end: index + 1 < matches.length ? matches[index + 1].index : contextSection.length,
|
|
174
|
+
start: match.index ?? 0,
|
|
175
|
+
end: index + 1 < matches.length ? (matches[index + 1].index ?? contextSection.length) : contextSection.length,
|
|
111
176
|
}));
|
|
112
177
|
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} prompt
|
|
181
|
+
* @param {string[]} [globalPaths]
|
|
182
|
+
* @returns {StripResult}
|
|
183
|
+
*/
|
|
113
184
|
const stripGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
|
|
114
185
|
const sectionStart = prompt.lastIndexOf(PROJECT_CONTEXT_HEADER);
|
|
115
186
|
if (sectionStart === -1) {
|
|
@@ -123,7 +194,9 @@ const stripGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
|
|
|
123
194
|
return { prompt, removedPaths: [] };
|
|
124
195
|
}
|
|
125
196
|
const globalPathKeys = new Set(globalPaths.map(normalizePath));
|
|
197
|
+
/** @type {string[]} */
|
|
126
198
|
const keptBlocks = [];
|
|
199
|
+
/** @type {string[]} */
|
|
127
200
|
const removedPaths = [];
|
|
128
201
|
for (const block of blocks) {
|
|
129
202
|
const blockText = contextSection.slice(block.start, block.end);
|
|
@@ -146,14 +219,29 @@ const stripGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
|
|
|
146
219
|
removedPaths: uniqueSorted(removedPaths),
|
|
147
220
|
};
|
|
148
221
|
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} start
|
|
225
|
+
* @returns {string | undefined}
|
|
226
|
+
*/
|
|
149
227
|
const getGitTopLevel = (start) => {
|
|
150
228
|
const topLevel = runGit(start, ["rev-parse", "--show-toplevel"]);
|
|
151
229
|
return topLevel ? normalizePath(topLevel) : undefined;
|
|
152
230
|
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @param {string} start
|
|
234
|
+
* @returns {string | undefined}
|
|
235
|
+
*/
|
|
153
236
|
const getGitCommonDir = (start) => {
|
|
154
237
|
const commonDir = runGit(start, ["rev-parse", "--git-common-dir"]);
|
|
155
238
|
return commonDir ? normalizePath(resolve(start, commonDir)) : undefined;
|
|
156
239
|
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {string} start
|
|
243
|
+
* @returns {string[]}
|
|
244
|
+
*/
|
|
157
245
|
const getWorktreeRoots = (start) => {
|
|
158
246
|
const list = runGit(start, ["worktree", "list", "--porcelain"]);
|
|
159
247
|
if (!list) {
|
|
@@ -166,6 +254,11 @@ const getWorktreeRoots = (start) => {
|
|
|
166
254
|
.map((line) => line.slice("worktree ".length)),
|
|
167
255
|
);
|
|
168
256
|
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @param {string} [start]
|
|
260
|
+
* @returns {ProjectState}
|
|
261
|
+
*/
|
|
169
262
|
const getProjectState = (start = process.cwd()) => {
|
|
170
263
|
const normalizedStart = normalizePath(start);
|
|
171
264
|
const gitTopLevel = getGitTopLevel(normalizedStart);
|
|
@@ -183,19 +276,32 @@ const getProjectState = (start = process.cwd()) => {
|
|
|
183
276
|
worktreeRoots.length > 0 ? uniqueSorted([projectRoot, ...worktreeRoots]) : [normalizePath(projectRoot)],
|
|
184
277
|
};
|
|
185
278
|
};
|
|
279
|
+
|
|
280
|
+
/** @param {ProjectState} state */
|
|
186
281
|
const getMarkerRoots = (state) => uniqueSorted([state.projectRoot, ...state.worktreeRoots]);
|
|
282
|
+
|
|
283
|
+
/** @param {ProjectState} state */
|
|
187
284
|
const hasMarker = (state) => getMarkerRoots(state).some((root) => existsSync(getMarkerPath(root)));
|
|
285
|
+
|
|
286
|
+
/** @param {ProjectState} state */
|
|
188
287
|
const writeMarkers = (state) => {
|
|
189
288
|
for (const root of getMarkerRoots(state)) {
|
|
190
289
|
mkdirSync(dirname(getMarkerPath(root)), { recursive: true });
|
|
191
290
|
writeFileSync(getMarkerPath(root), "\n");
|
|
192
291
|
}
|
|
193
292
|
};
|
|
293
|
+
|
|
294
|
+
/** @param {ProjectState} state */
|
|
194
295
|
const clearMarkers = (state) => {
|
|
195
296
|
for (const root of getMarkerRoots(state)) {
|
|
196
297
|
rmSync(getMarkerPath(root), { force: true });
|
|
197
298
|
}
|
|
198
299
|
};
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {string[]} [paths]
|
|
303
|
+
* @returns {string}
|
|
304
|
+
*/
|
|
199
305
|
const buildLocalOnlyNotice = (paths = getExistingGlobalContextPaths(getAgentDir())) => {
|
|
200
306
|
if (paths.length === 0) {
|
|
201
307
|
return "";
|
|
@@ -208,6 +314,12 @@ const buildLocalOnlyNotice = (paths = getExistingGlobalContextPaths(getAgentDir(
|
|
|
208
314
|
"Follow only repo-local AGENTS.md or CLAUDE.md guidance for this project.",
|
|
209
315
|
].join("\n");
|
|
210
316
|
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @param {string} prompt
|
|
320
|
+
* @param {string} [agentDir]
|
|
321
|
+
* @returns {string}
|
|
322
|
+
*/
|
|
211
323
|
const applyLocalOnlyPrompt = (prompt, agentDir = getAgentDir()) => {
|
|
212
324
|
const { prompt: stripped, removedPaths } = stripGlobalContext(prompt, getGlobalContextPaths(agentDir));
|
|
213
325
|
const notice = buildLocalOnlyNotice(
|
|
@@ -215,6 +327,8 @@ const applyLocalOnlyPrompt = (prompt, agentDir = getAgentDir()) => {
|
|
|
215
327
|
);
|
|
216
328
|
return notice ? `${stripped}\n\n${notice}` : stripped;
|
|
217
329
|
};
|
|
330
|
+
|
|
331
|
+
/** @param {ExtensionContext} ctx */
|
|
218
332
|
const setStatus = (ctx) => {
|
|
219
333
|
if (!ctx.hasUI) {
|
|
220
334
|
return;
|
|
@@ -222,8 +336,18 @@ const setStatus = (ctx) => {
|
|
|
222
336
|
const mode = getMode(ctx.cwd);
|
|
223
337
|
ctx.ui.setStatus(COMMAND, mode.enabled ? `AGENTS: local-only (${mode.source})` : undefined);
|
|
224
338
|
};
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @param {ProjectState} state
|
|
342
|
+
* @returns {string}
|
|
343
|
+
*/
|
|
225
344
|
const getProjectTarget = (state) =>
|
|
226
345
|
state.worktreeRoots.length > 1 ? `${state.projectRoot} and linked worktrees` : state.projectRoot;
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* @param {ProjectState} state
|
|
349
|
+
* @returns {string}
|
|
350
|
+
*/
|
|
227
351
|
const getOffNotification = (state) => {
|
|
228
352
|
const mode = getMode(state);
|
|
229
353
|
if (!mode.enabled) {
|
|
@@ -238,10 +362,20 @@ const getOffNotification = (state) => {
|
|
|
238
362
|
return `Repo marker cleared for ${getProjectTarget(state)}, but local-agents-only is still enabled via ${mode.source}.`;
|
|
239
363
|
};
|
|
240
364
|
|
|
365
|
+
/**
|
|
366
|
+
* @param {string} [start]
|
|
367
|
+
* @returns {string}
|
|
368
|
+
*/
|
|
241
369
|
export function findProjectRoot(start = process.cwd()) {
|
|
242
370
|
return getProjectState(start).projectRoot;
|
|
243
371
|
}
|
|
244
372
|
|
|
373
|
+
/**
|
|
374
|
+
* @param {string | ProjectState} [start]
|
|
375
|
+
* @param {string | undefined} [envValue]
|
|
376
|
+
* @param {string} [configPath]
|
|
377
|
+
* @returns {Mode}
|
|
378
|
+
*/
|
|
245
379
|
export function getMode(start = process.cwd(), envValue = process.env.PI_LOCAL_AGENTS_ONLY, configPath = CONFIG()) {
|
|
246
380
|
const state = typeof start === "string" ? getProjectState(start) : start;
|
|
247
381
|
const envToggle = getEnvToggle(envValue);
|
|
@@ -261,10 +395,16 @@ export function getMode(start = process.cwd(), envValue = process.env.PI_LOCAL_A
|
|
|
261
395
|
return { enabled: false, source: "default" };
|
|
262
396
|
}
|
|
263
397
|
|
|
398
|
+
/**
|
|
399
|
+
* @param {string} prompt
|
|
400
|
+
* @param {string[]} [globalPaths]
|
|
401
|
+
* @returns {string}
|
|
402
|
+
*/
|
|
264
403
|
export function stripGlobalBlocks(prompt, globalPaths = getGlobalContextPaths()) {
|
|
265
404
|
return stripGlobalContext(prompt, globalPaths).prompt;
|
|
266
405
|
}
|
|
267
406
|
|
|
407
|
+
/** @param {ExtensionAPI} pi */
|
|
268
408
|
export default function localAgentsOnly(pi) {
|
|
269
409
|
pi.registerCommand(COMMAND, {
|
|
270
410
|
description: "Use only repo-local AGENTS prompt context",
|
|
@@ -274,7 +414,10 @@ export default function localAgentsOnly(pi) {
|
|
|
274
414
|
case "on":
|
|
275
415
|
writeMarkers(state);
|
|
276
416
|
setStatus(ctx);
|
|
277
|
-
ctx.ui.notify(
|
|
417
|
+
ctx.ui.notify(
|
|
418
|
+
`Enabled for ${state.projectRoot}${state.worktreeRoots.length > 1 ? ` across ${state.worktreeRoots.length} worktrees` : ""}`,
|
|
419
|
+
"info",
|
|
420
|
+
);
|
|
278
421
|
return;
|
|
279
422
|
case "off":
|
|
280
423
|
clearMarkers(state);
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-local-agents-only",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"keywords": [
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi",
|
|
11
|
+
"pi-extension",
|
|
12
|
+
"extension"
|
|
13
|
+
],
|
|
9
14
|
"repository": {
|
|
10
15
|
"type": "git",
|
|
11
16
|
"url": "git+https://github.com/fitchmultz/pi-local-agents-only.git"
|
|
@@ -14,11 +19,25 @@
|
|
|
14
19
|
"url": "https://github.com/fitchmultz/pi-local-agents-only/issues"
|
|
15
20
|
},
|
|
16
21
|
"homepage": "https://github.com/fitchmultz/pi-local-agents-only#readme",
|
|
17
|
-
"files": [
|
|
22
|
+
"files": [
|
|
23
|
+
"extensions",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
18
28
|
"pi": {
|
|
19
|
-
"extensions": [
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./extensions"
|
|
31
|
+
]
|
|
20
32
|
},
|
|
21
33
|
"scripts": {
|
|
22
|
-
"test": "node --test"
|
|
34
|
+
"test": "node --test",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"check": "npm test && npm run typecheck"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@mariozechner/pi-coding-agent": "^0.65.2",
|
|
40
|
+
"@types/node": "^24.3.0",
|
|
41
|
+
"typescript": "^5.7.3"
|
|
23
42
|
}
|
|
24
43
|
}
|