pi-local-agents-only 0.1.5 → 0.1.7
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 +15 -0
- package/extensions/local-agents-only.js +303 -25
- package/package.json +25 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.7 - 2026-04-11
|
|
4
|
+
|
|
5
|
+
- fix non-git project-root detection so the global `~/.pi` directory no longer hijacks repo inference under a user's home directory
|
|
6
|
+
- make the integration tests hermetic by resolving the repo-local `@mariozechner/pi-coding-agent` install instead of a global npm install
|
|
7
|
+
- refuse to overwrite malformed global allowlist config and write config updates atomically
|
|
8
|
+
- add regression tests for the homedir root bug and config-write hardening
|
|
9
|
+
- add GitHub Actions CI plus a `prepublishOnly` guard that reruns `npm run check` before publish
|
|
10
|
+
|
|
11
|
+
## 0.1.6 - 2026-04-07
|
|
12
|
+
|
|
13
|
+
- add dev-time static checking with `tsc --noEmit`
|
|
14
|
+
- add a local `check` script that runs tests plus typechecking
|
|
15
|
+
- keep pi core typing support in `devDependencies` only so this stays a lightweight published package
|
|
16
|
+
- annotate the extension with `// @ts-check` and JSDoc types for earlier local error detection
|
|
17
|
+
|
|
3
18
|
## 0.1.5 - 2026-04-07
|
|
4
19
|
|
|
5
20
|
- 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.
|
|
@@ -7,10 +9,29 @@
|
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import { execFileSync } from "node:child_process";
|
|
10
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
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
|
+
|
|
24
|
+
class ConfigError extends Error {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} message
|
|
27
|
+
* @param {unknown} [cause]
|
|
28
|
+
*/
|
|
29
|
+
constructor(message, cause) {
|
|
30
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
31
|
+
this.name = "ConfigError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
14
35
|
const COMMAND = "local-agents-only";
|
|
15
36
|
const MARKER = join(".pi", COMMAND);
|
|
16
37
|
const GLOBAL_CONTEXT_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
@@ -20,7 +41,9 @@ const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instru
|
|
|
20
41
|
const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
|
|
21
42
|
const DATE_HEADER = "\nCurrent date:";
|
|
22
43
|
const CONTEXT_BLOCK_HEADER = /^## ([^\n]+(?:AGENTS|CLAUDE)\.md)\n\n/gm;
|
|
44
|
+
const emptyConfig = () => ({ projects: [], repositories: [] });
|
|
23
45
|
|
|
46
|
+
/** @returns {string} */
|
|
24
47
|
const getAgentDir = () => {
|
|
25
48
|
const env = process.env.PI_CODING_AGENT_DIR;
|
|
26
49
|
if (env === "~") {
|
|
@@ -31,10 +54,31 @@ const getAgentDir = () => {
|
|
|
31
54
|
}
|
|
32
55
|
return env || join(homedir(), ".pi", "agent");
|
|
33
56
|
};
|
|
57
|
+
|
|
58
|
+
/** @param {string} path */
|
|
34
59
|
const normalizePath = (path) => resolve(path).replace(/\\/g, "/");
|
|
60
|
+
|
|
61
|
+
/** @param {string} path */
|
|
62
|
+
const isGlobalPiDirectory = (path) => {
|
|
63
|
+
const normalizedPath = normalizePath(path);
|
|
64
|
+
const agentDir = normalizePath(getAgentDir());
|
|
65
|
+
return normalizedPath === agentDir || normalizedPath === normalizePath(dirname(agentDir));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** @returns {string} */
|
|
35
69
|
const CONFIG = () => join(getAgentDir(), `${COMMAND}.json`);
|
|
70
|
+
|
|
71
|
+
/** @param {string} projectRoot */
|
|
36
72
|
const getMarkerPath = (projectRoot) => join(projectRoot, MARKER);
|
|
73
|
+
|
|
74
|
+
/** @param {string[]} values */
|
|
37
75
|
const uniqueSorted = (values) => [...new Set(values.map(normalizePath))].sort();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} start
|
|
79
|
+
* @param {(dir: string) => boolean} predicate
|
|
80
|
+
* @returns {string | undefined}
|
|
81
|
+
*/
|
|
38
82
|
const walkUp = (start, predicate) => {
|
|
39
83
|
let current = resolve(start);
|
|
40
84
|
while (true) {
|
|
@@ -43,11 +87,17 @@ const walkUp = (start, predicate) => {
|
|
|
43
87
|
}
|
|
44
88
|
const parent = dirname(current);
|
|
45
89
|
if (parent === current) {
|
|
46
|
-
return;
|
|
90
|
+
return undefined;
|
|
47
91
|
}
|
|
48
92
|
current = parent;
|
|
49
93
|
}
|
|
50
94
|
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {string} start
|
|
98
|
+
* @param {string[]} args
|
|
99
|
+
* @returns {string | undefined}
|
|
100
|
+
*/
|
|
51
101
|
const runGit = (start, args) => {
|
|
52
102
|
try {
|
|
53
103
|
return execFileSync("git", args, {
|
|
@@ -56,23 +106,118 @@ const runGit = (start, args) => {
|
|
|
56
106
|
stdio: ["ignore", "pipe", "ignore"],
|
|
57
107
|
}).trim();
|
|
58
108
|
} catch {
|
|
59
|
-
return;
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} name
|
|
115
|
+
* @param {unknown} value
|
|
116
|
+
* @param {string} configPath
|
|
117
|
+
* @returns {string[]}
|
|
118
|
+
*/
|
|
119
|
+
const parseConfigList = (name, value, configPath) => {
|
|
120
|
+
if (value === undefined) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
|
|
124
|
+
throw new ConfigError(
|
|
125
|
+
`Malformed local-agents-only config at ${normalizePath(configPath)}. Expected "${name}" to be an array of strings. Fix or remove the file, then retry.`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return uniqueSorted(value);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} rawConfig
|
|
133
|
+
* @param {string} configPath
|
|
134
|
+
* @returns {LocalAgentsOnlyConfig}
|
|
135
|
+
*/
|
|
136
|
+
const parseConfig = (rawConfig, configPath) => {
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = /** @type {{ projects?: unknown; repositories?: unknown }} */ (JSON.parse(rawConfig));
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new ConfigError(
|
|
142
|
+
`Malformed local-agents-only config at ${normalizePath(configPath)}. Fix or remove the file, then retry.`,
|
|
143
|
+
error,
|
|
144
|
+
);
|
|
60
145
|
}
|
|
146
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
147
|
+
throw new ConfigError(
|
|
148
|
+
`Malformed local-agents-only config at ${normalizePath(configPath)}. Expected a JSON object. Fix or remove the file, then retry.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
projects: parseConfigList("projects", parsed.projects, configPath),
|
|
153
|
+
repositories: parseConfigList("repositories", parsed.repositories, configPath),
|
|
154
|
+
};
|
|
61
155
|
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {string} [configPath]
|
|
159
|
+
* @returns {LocalAgentsOnlyConfig}
|
|
160
|
+
*/
|
|
161
|
+
const readConfigForMutation = (configPath = CONFIG()) => {
|
|
162
|
+
if (!existsSync(configPath)) {
|
|
163
|
+
return emptyConfig();
|
|
164
|
+
}
|
|
165
|
+
return parseConfig(readFileSync(configPath, "utf8"), configPath);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {string} [configPath]
|
|
170
|
+
* @returns {LocalAgentsOnlyConfig}
|
|
171
|
+
*/
|
|
62
172
|
const readConfig = (configPath = CONFIG()) => {
|
|
63
173
|
try {
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
projects: Array.isArray(projects) ? projects.map(normalizePath) : [],
|
|
67
|
-
repositories: Array.isArray(repositories) ? repositories.map(normalizePath) : [],
|
|
68
|
-
};
|
|
174
|
+
return readConfigForMutation(configPath);
|
|
69
175
|
} catch {
|
|
70
|
-
return
|
|
176
|
+
return emptyConfig();
|
|
71
177
|
}
|
|
72
178
|
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} path
|
|
182
|
+
* @param {string} content
|
|
183
|
+
*/
|
|
184
|
+
const writeFileAtomically = (path, content) => {
|
|
185
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
186
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
187
|
+
/** @type {number | undefined} */
|
|
188
|
+
let fileDescriptor;
|
|
189
|
+
try {
|
|
190
|
+
fileDescriptor = openSync(tempPath, "wx", 0o600);
|
|
191
|
+
writeFileSync(fileDescriptor, content, "utf8");
|
|
192
|
+
fsyncSync(fileDescriptor);
|
|
193
|
+
closeSync(fileDescriptor);
|
|
194
|
+
fileDescriptor = undefined;
|
|
195
|
+
renameSync(tempPath, path);
|
|
196
|
+
try {
|
|
197
|
+
const directoryDescriptor = openSync(dirname(path), "r");
|
|
198
|
+
try {
|
|
199
|
+
fsyncSync(directoryDescriptor);
|
|
200
|
+
} finally {
|
|
201
|
+
closeSync(directoryDescriptor);
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// Best effort: directory fsync is not available on every platform.
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (fileDescriptor !== undefined) {
|
|
208
|
+
closeSync(fileDescriptor);
|
|
209
|
+
}
|
|
210
|
+
rmSync(tempPath, { force: true });
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @param {LocalAgentsOnlyConfig} config
|
|
217
|
+
* @param {string} [configPath]
|
|
218
|
+
*/
|
|
73
219
|
const writeConfig = ({ projects, repositories }, configPath = CONFIG()) => {
|
|
74
|
-
|
|
75
|
-
writeFileSync(
|
|
220
|
+
writeFileAtomically(
|
|
76
221
|
configPath,
|
|
77
222
|
JSON.stringify(
|
|
78
223
|
{
|
|
@@ -84,6 +229,11 @@ const writeConfig = ({ projects, repositories }, configPath = CONFIG()) => {
|
|
|
84
229
|
) + "\n",
|
|
85
230
|
);
|
|
86
231
|
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {string | undefined} [value]
|
|
235
|
+
* @returns {boolean | undefined}
|
|
236
|
+
*/
|
|
87
237
|
const getEnvToggle = (value = process.env.PI_LOCAL_AGENTS_ONLY) => {
|
|
88
238
|
const toggle = `${value ?? ""}`.trim().toLowerCase();
|
|
89
239
|
if (ENV_TRUE.includes(toggle)) {
|
|
@@ -92,24 +242,46 @@ const getEnvToggle = (value = process.env.PI_LOCAL_AGENTS_ONLY) => {
|
|
|
92
242
|
if (ENV_FALSE.includes(toggle)) {
|
|
93
243
|
return false;
|
|
94
244
|
}
|
|
245
|
+
return undefined;
|
|
95
246
|
};
|
|
247
|
+
|
|
248
|
+
/** @param {string} [agentDir] */
|
|
96
249
|
const getGlobalContextPaths = (agentDir = getAgentDir()) => GLOBAL_CONTEXT_FILES.map((name) => join(agentDir, name));
|
|
250
|
+
|
|
251
|
+
/** @param {string} [agentDir] */
|
|
97
252
|
const getExistingGlobalContextPaths = (agentDir = getAgentDir()) =>
|
|
98
253
|
getGlobalContextPaths(agentDir).filter((path) => existsSync(path));
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {string} prompt
|
|
257
|
+
* @param {number} offset
|
|
258
|
+
* @returns {number}
|
|
259
|
+
*/
|
|
99
260
|
const getContextSectionEnd = (prompt, offset) => {
|
|
100
261
|
const candidates = [prompt.indexOf(SKILLS_HEADER, offset), prompt.indexOf(DATE_HEADER, offset)].filter(
|
|
101
262
|
(index) => index !== -1,
|
|
102
263
|
);
|
|
103
264
|
return candidates.length > 0 ? Math.min(...candidates) : prompt.length;
|
|
104
265
|
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @param {string} contextSection
|
|
269
|
+
* @returns {ContextBlock[]}
|
|
270
|
+
*/
|
|
105
271
|
const getContextBlocks = (contextSection) => {
|
|
106
272
|
const matches = [...contextSection.matchAll(CONTEXT_BLOCK_HEADER)];
|
|
107
273
|
return matches.map((match, index) => ({
|
|
108
274
|
path: match[1],
|
|
109
|
-
start: match.index,
|
|
110
|
-
end: index + 1 < matches.length ? matches[index + 1].index : contextSection.length,
|
|
275
|
+
start: match.index ?? 0,
|
|
276
|
+
end: index + 1 < matches.length ? (matches[index + 1].index ?? contextSection.length) : contextSection.length,
|
|
111
277
|
}));
|
|
112
278
|
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {string} prompt
|
|
282
|
+
* @param {string[]} [globalPaths]
|
|
283
|
+
* @returns {StripResult}
|
|
284
|
+
*/
|
|
113
285
|
const stripGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
|
|
114
286
|
const sectionStart = prompt.lastIndexOf(PROJECT_CONTEXT_HEADER);
|
|
115
287
|
if (sectionStart === -1) {
|
|
@@ -123,7 +295,9 @@ const stripGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
|
|
|
123
295
|
return { prompt, removedPaths: [] };
|
|
124
296
|
}
|
|
125
297
|
const globalPathKeys = new Set(globalPaths.map(normalizePath));
|
|
298
|
+
/** @type {string[]} */
|
|
126
299
|
const keptBlocks = [];
|
|
300
|
+
/** @type {string[]} */
|
|
127
301
|
const removedPaths = [];
|
|
128
302
|
for (const block of blocks) {
|
|
129
303
|
const blockText = contextSection.slice(block.start, block.end);
|
|
@@ -146,14 +320,29 @@ const stripGlobalContext = (prompt, globalPaths = getGlobalContextPaths()) => {
|
|
|
146
320
|
removedPaths: uniqueSorted(removedPaths),
|
|
147
321
|
};
|
|
148
322
|
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {string} start
|
|
326
|
+
* @returns {string | undefined}
|
|
327
|
+
*/
|
|
149
328
|
const getGitTopLevel = (start) => {
|
|
150
329
|
const topLevel = runGit(start, ["rev-parse", "--show-toplevel"]);
|
|
151
330
|
return topLevel ? normalizePath(topLevel) : undefined;
|
|
152
331
|
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @param {string} start
|
|
335
|
+
* @returns {string | undefined}
|
|
336
|
+
*/
|
|
153
337
|
const getGitCommonDir = (start) => {
|
|
154
338
|
const commonDir = runGit(start, ["rev-parse", "--git-common-dir"]);
|
|
155
339
|
return commonDir ? normalizePath(resolve(start, commonDir)) : undefined;
|
|
156
340
|
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {string} start
|
|
344
|
+
* @returns {string[]}
|
|
345
|
+
*/
|
|
157
346
|
const getWorktreeRoots = (start) => {
|
|
158
347
|
const list = runGit(start, ["worktree", "list", "--porcelain"]);
|
|
159
348
|
if (!list) {
|
|
@@ -166,13 +355,21 @@ const getWorktreeRoots = (start) => {
|
|
|
166
355
|
.map((line) => line.slice("worktree ".length)),
|
|
167
356
|
);
|
|
168
357
|
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @param {string} [start]
|
|
361
|
+
* @returns {ProjectState}
|
|
362
|
+
*/
|
|
169
363
|
const getProjectState = (start = process.cwd()) => {
|
|
170
364
|
const normalizedStart = normalizePath(start);
|
|
171
365
|
const gitTopLevel = getGitTopLevel(normalizedStart);
|
|
172
366
|
const projectRoot =
|
|
173
367
|
gitTopLevel ||
|
|
174
368
|
walkUp(normalizedStart, (dir) => existsSync(getMarkerPath(dir))) ||
|
|
175
|
-
walkUp(normalizedStart, (dir) =>
|
|
369
|
+
walkUp(normalizedStart, (dir) => {
|
|
370
|
+
const piDir = join(dir, ".pi");
|
|
371
|
+
return existsSync(piDir) && !isGlobalPiDirectory(piDir);
|
|
372
|
+
}) ||
|
|
176
373
|
normalizedStart;
|
|
177
374
|
const worktreeRoots = getWorktreeRoots(normalizedStart);
|
|
178
375
|
return {
|
|
@@ -183,19 +380,32 @@ const getProjectState = (start = process.cwd()) => {
|
|
|
183
380
|
worktreeRoots.length > 0 ? uniqueSorted([projectRoot, ...worktreeRoots]) : [normalizePath(projectRoot)],
|
|
184
381
|
};
|
|
185
382
|
};
|
|
383
|
+
|
|
384
|
+
/** @param {ProjectState} state */
|
|
186
385
|
const getMarkerRoots = (state) => uniqueSorted([state.projectRoot, ...state.worktreeRoots]);
|
|
386
|
+
|
|
387
|
+
/** @param {ProjectState} state */
|
|
187
388
|
const hasMarker = (state) => getMarkerRoots(state).some((root) => existsSync(getMarkerPath(root)));
|
|
389
|
+
|
|
390
|
+
/** @param {ProjectState} state */
|
|
188
391
|
const writeMarkers = (state) => {
|
|
189
392
|
for (const root of getMarkerRoots(state)) {
|
|
190
393
|
mkdirSync(dirname(getMarkerPath(root)), { recursive: true });
|
|
191
394
|
writeFileSync(getMarkerPath(root), "\n");
|
|
192
395
|
}
|
|
193
396
|
};
|
|
397
|
+
|
|
398
|
+
/** @param {ProjectState} state */
|
|
194
399
|
const clearMarkers = (state) => {
|
|
195
400
|
for (const root of getMarkerRoots(state)) {
|
|
196
401
|
rmSync(getMarkerPath(root), { force: true });
|
|
197
402
|
}
|
|
198
403
|
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @param {string[]} [paths]
|
|
407
|
+
* @returns {string}
|
|
408
|
+
*/
|
|
199
409
|
const buildLocalOnlyNotice = (paths = getExistingGlobalContextPaths(getAgentDir())) => {
|
|
200
410
|
if (paths.length === 0) {
|
|
201
411
|
return "";
|
|
@@ -208,6 +418,12 @@ const buildLocalOnlyNotice = (paths = getExistingGlobalContextPaths(getAgentDir(
|
|
|
208
418
|
"Follow only repo-local AGENTS.md or CLAUDE.md guidance for this project.",
|
|
209
419
|
].join("\n");
|
|
210
420
|
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @param {string} prompt
|
|
424
|
+
* @param {string} [agentDir]
|
|
425
|
+
* @returns {string}
|
|
426
|
+
*/
|
|
211
427
|
const applyLocalOnlyPrompt = (prompt, agentDir = getAgentDir()) => {
|
|
212
428
|
const { prompt: stripped, removedPaths } = stripGlobalContext(prompt, getGlobalContextPaths(agentDir));
|
|
213
429
|
const notice = buildLocalOnlyNotice(
|
|
@@ -215,6 +431,8 @@ const applyLocalOnlyPrompt = (prompt, agentDir = getAgentDir()) => {
|
|
|
215
431
|
);
|
|
216
432
|
return notice ? `${stripped}\n\n${notice}` : stripped;
|
|
217
433
|
};
|
|
434
|
+
|
|
435
|
+
/** @param {ExtensionContext} ctx */
|
|
218
436
|
const setStatus = (ctx) => {
|
|
219
437
|
if (!ctx.hasUI) {
|
|
220
438
|
return;
|
|
@@ -222,8 +440,18 @@ const setStatus = (ctx) => {
|
|
|
222
440
|
const mode = getMode(ctx.cwd);
|
|
223
441
|
ctx.ui.setStatus(COMMAND, mode.enabled ? `AGENTS: local-only (${mode.source})` : undefined);
|
|
224
442
|
};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* @param {ProjectState} state
|
|
446
|
+
* @returns {string}
|
|
447
|
+
*/
|
|
225
448
|
const getProjectTarget = (state) =>
|
|
226
449
|
state.worktreeRoots.length > 1 ? `${state.projectRoot} and linked worktrees` : state.projectRoot;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* @param {ProjectState} state
|
|
453
|
+
* @returns {string}
|
|
454
|
+
*/
|
|
227
455
|
const getOffNotification = (state) => {
|
|
228
456
|
const mode = getMode(state);
|
|
229
457
|
if (!mode.enabled) {
|
|
@@ -238,10 +466,43 @@ const getOffNotification = (state) => {
|
|
|
238
466
|
return `Repo marker cleared for ${getProjectTarget(state)}, but local-agents-only is still enabled via ${mode.source}.`;
|
|
239
467
|
};
|
|
240
468
|
|
|
469
|
+
/**
|
|
470
|
+
* @param {(config: LocalAgentsOnlyConfig) => LocalAgentsOnlyConfig} mutate
|
|
471
|
+
*/
|
|
472
|
+
const mutateGlobalConfig = (mutate) => {
|
|
473
|
+
const configPath = CONFIG();
|
|
474
|
+
const config = readConfigForMutation(configPath);
|
|
475
|
+
writeConfig(mutate(config), configPath);
|
|
476
|
+
return configPath;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* @param {unknown} error
|
|
481
|
+
* @param {string} [configPath]
|
|
482
|
+
* @returns {string}
|
|
483
|
+
*/
|
|
484
|
+
const getGlobalConfigMutationError = (error, configPath = CONFIG()) => {
|
|
485
|
+
if (error instanceof ConfigError) {
|
|
486
|
+
return `Global allowlist unchanged: ${error.message}`;
|
|
487
|
+
}
|
|
488
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
489
|
+
return `Global allowlist unchanged: failed to update ${normalizePath(configPath)} (${reason}).`;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @param {string} [start]
|
|
494
|
+
* @returns {string}
|
|
495
|
+
*/
|
|
241
496
|
export function findProjectRoot(start = process.cwd()) {
|
|
242
497
|
return getProjectState(start).projectRoot;
|
|
243
498
|
}
|
|
244
499
|
|
|
500
|
+
/**
|
|
501
|
+
* @param {string | ProjectState} [start]
|
|
502
|
+
* @param {string | undefined} [envValue]
|
|
503
|
+
* @param {string} [configPath]
|
|
504
|
+
* @returns {Mode}
|
|
505
|
+
*/
|
|
245
506
|
export function getMode(start = process.cwd(), envValue = process.env.PI_LOCAL_AGENTS_ONLY, configPath = CONFIG()) {
|
|
246
507
|
const state = typeof start === "string" ? getProjectState(start) : start;
|
|
247
508
|
const envToggle = getEnvToggle(envValue);
|
|
@@ -261,10 +522,16 @@ export function getMode(start = process.cwd(), envValue = process.env.PI_LOCAL_A
|
|
|
261
522
|
return { enabled: false, source: "default" };
|
|
262
523
|
}
|
|
263
524
|
|
|
525
|
+
/**
|
|
526
|
+
* @param {string} prompt
|
|
527
|
+
* @param {string[]} [globalPaths]
|
|
528
|
+
* @returns {string}
|
|
529
|
+
*/
|
|
264
530
|
export function stripGlobalBlocks(prompt, globalPaths = getGlobalContextPaths()) {
|
|
265
531
|
return stripGlobalContext(prompt, globalPaths).prompt;
|
|
266
532
|
}
|
|
267
533
|
|
|
534
|
+
/** @param {ExtensionAPI} pi */
|
|
268
535
|
export default function localAgentsOnly(pi) {
|
|
269
536
|
pi.registerCommand(COMMAND, {
|
|
270
537
|
description: "Use only repo-local AGENTS prompt context",
|
|
@@ -274,7 +541,10 @@ export default function localAgentsOnly(pi) {
|
|
|
274
541
|
case "on":
|
|
275
542
|
writeMarkers(state);
|
|
276
543
|
setStatus(ctx);
|
|
277
|
-
ctx.ui.notify(
|
|
544
|
+
ctx.ui.notify(
|
|
545
|
+
`Enabled for ${state.projectRoot}${state.worktreeRoots.length > 1 ? ` across ${state.worktreeRoots.length} worktrees` : ""}`,
|
|
546
|
+
"info",
|
|
547
|
+
);
|
|
278
548
|
return;
|
|
279
549
|
case "off":
|
|
280
550
|
clearMarkers(state);
|
|
@@ -282,21 +552,29 @@ export default function localAgentsOnly(pi) {
|
|
|
282
552
|
ctx.ui.notify(getOffNotification(state), "info");
|
|
283
553
|
return;
|
|
284
554
|
case "global-on": {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
555
|
+
try {
|
|
556
|
+
mutateGlobalConfig((config) => ({
|
|
557
|
+
projects: [...config.projects, ...state.worktreeRoots],
|
|
558
|
+
repositories: [...config.repositories, state.repoId],
|
|
559
|
+
}));
|
|
560
|
+
} catch (error) {
|
|
561
|
+
ctx.ui.notify(getGlobalConfigMutationError(error), "error");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
290
564
|
setStatus(ctx);
|
|
291
565
|
ctx.ui.notify(`Global allowlist enabled for ${state.projectRoot}`, "info");
|
|
292
566
|
return;
|
|
293
567
|
}
|
|
294
568
|
case "global-off": {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
569
|
+
try {
|
|
570
|
+
mutateGlobalConfig((config) => ({
|
|
571
|
+
projects: config.projects.filter((path) => !state.worktreeRoots.includes(path)),
|
|
572
|
+
repositories: config.repositories.filter((id) => id !== state.repoId),
|
|
573
|
+
}));
|
|
574
|
+
} catch (error) {
|
|
575
|
+
ctx.ui.notify(getGlobalConfigMutationError(error), "error");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
300
578
|
setStatus(ctx);
|
|
301
579
|
ctx.ui.notify(`Global allowlist disabled for ${state.projectRoot}`, "info");
|
|
302
580
|
return;
|
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.7",
|
|
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,26 @@
|
|
|
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
|
+
"prepublishOnly": "npm run check"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@mariozechner/pi-coding-agent": "^0.65.2",
|
|
41
|
+
"@types/node": "^24.3.0",
|
|
42
|
+
"typescript": "^5.7.3"
|
|
23
43
|
}
|
|
24
44
|
}
|