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 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
- const { projects = [], repositories = [] } = JSON.parse(readFileSync(configPath, "utf8"));
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 { projects: [], repositories: [] };
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
- mkdirSync(dirname(configPath), { recursive: true });
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) => existsSync(join(dir, ".pi"))) ||
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(`Enabled for ${state.projectRoot}${state.worktreeRoots.length > 1 ? ` across ${state.worktreeRoots.length} worktrees` : ""}`, "info");
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
- const config = readConfig();
286
- writeConfig({
287
- projects: [...config.projects, ...state.worktreeRoots],
288
- repositories: [...config.repositories, state.repoId],
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
- const config = readConfig();
296
- writeConfig({
297
- projects: config.projects.filter((path) => !state.worktreeRoots.includes(path)),
298
- repositories: config.repositories.filter((id) => id !== state.repoId),
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.5",
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": ["pi-package", "pi", "pi-extension", "extension"],
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": ["extensions", "README.md", "CHANGELOG.md", "LICENSE"],
22
+ "files": [
23
+ "extensions",
24
+ "README.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
18
28
  "pi": {
19
- "extensions": ["./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
  }