@gotgenes/pi-permission-system 5.11.1 → 5.14.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/CHANGELOG.md CHANGED
@@ -5,6 +5,72 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.14.0](https://github.com/gotgenes/pi-permission-system/compare/v5.13.0...v5.14.0) (2026-05-09)
9
+
10
+
11
+ ### Features
12
+
13
+ * support ? single-character wildcard in permission patterns ([#122](https://github.com/gotgenes/pi-permission-system/issues/122)) ([7b56f49](https://github.com/gotgenes/pi-permission-system/commit/7b56f4979a3479912dcc1d903f52a517587b95f6))
14
+
15
+
16
+ ### Documentation
17
+
18
+ * document ? wildcard and update OpenCode compatibility ([#122](https://github.com/gotgenes/pi-permission-system/issues/122)) ([31ace5f](https://github.com/gotgenes/pi-permission-system/commit/31ace5f36a11299750994c2b9e6084dbecc240ba))
19
+ * plan ? single-character wildcard support ([#122](https://github.com/gotgenes/pi-permission-system/issues/122)) ([a7a2963](https://github.com/gotgenes/pi-permission-system/commit/a7a296337809ee7c107f86b87b64bfeb3709467f))
20
+ * **retro:** add retro notes for issue [#1](https://github.com/gotgenes/pi-permission-system/issues/1) ([b1c66f1](https://github.com/gotgenes/pi-permission-system/commit/b1c66f18d1aed5cc95a1fccb1a9c6c0f44f5cd11))
21
+
22
+ ## [5.13.0](https://github.com/gotgenes/pi-permission-system/compare/v5.12.0...v5.13.0) (2026-05-08)
23
+
24
+
25
+ ### Features
26
+
27
+ * warn that this is a pnpm project on global npm pass-throughs ([f643149](https://github.com/gotgenes/pi-permission-system/commit/f64314981126331ec96ec6a20418440eff1738e7))
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * pass through npm install/uninstall -g in PATH shim ([eaf4256](https://github.com/gotgenes/pi-permission-system/commit/eaf4256446b2c1ec3ecba98d11cc75b1406931af))
33
+ * prevent double-loading extension in dev via project settings ([6c39f33](https://github.com/gotgenes/pi-permission-system/commit/6c39f33890e61b5e0fde41d178aad9df93592cc9))
34
+
35
+
36
+ ### Documentation
37
+
38
+ * plan external_directory integration tests ([#1](https://github.com/gotgenes/pi-permission-system/issues/1)) ([695ffeb](https://github.com/gotgenes/pi-permission-system/commit/695ffeb6d7a648df3dd9e18a01c20ff22266857b))
39
+ * **retro:** add retro notes for double-prompt investigation ([37734a5](https://github.com/gotgenes/pi-permission-system/commit/37734a57aee972aa4c732d92eeda424dd40443ee))
40
+ * **retro:** add retro notes for issue [#123](https://github.com/gotgenes/pi-permission-system/issues/123) ([5dbea33](https://github.com/gotgenes/pi-permission-system/commit/5dbea3379963b6231a4a101c889a4594705b12b6))
41
+
42
+
43
+ ### Miscellaneous Chores
44
+
45
+ * switch ask tool from pi-ask-user to @eko24ive/pi-ask ([0087458](https://github.com/gotgenes/pi-permission-system/commit/00874585724d79fbeda1fca84b998db3bcd1a043))
46
+
47
+ ## [5.12.0](https://github.com/gotgenes/pi-permission-system/compare/v5.11.2...v5.12.0) (2026-05-08)
48
+
49
+
50
+ ### Features
51
+
52
+ * support trailing wildcard optionality ([#123](https://github.com/gotgenes/pi-permission-system/issues/123)) ([c25b0b5](https://github.com/gotgenes/pi-permission-system/commit/c25b0b5c59e5d5739a3b6c99de18444a4e7820ec))
53
+
54
+
55
+ ### Documentation
56
+
57
+ * plan trailing wildcard optionality ([#123](https://github.com/gotgenes/pi-permission-system/issues/123)) ([a6a50d2](https://github.com/gotgenes/pi-permission-system/commit/a6a50d28284bb624a3695d361b5b9f2a9861f470))
58
+ * **retro:** add retro notes for issue [#113](https://github.com/gotgenes/pi-permission-system/issues/113) ([7412740](https://github.com/gotgenes/pi-permission-system/commit/7412740c876c42cc1cd63f95ee4cb0ea0de72e68))
59
+ * update wildcard optionality docs ([#123](https://github.com/gotgenes/pi-permission-system/issues/123)) ([562adaf](https://github.com/gotgenes/pi-permission-system/commit/562adaff9726096004dc4f5214550af2dc2fc118))
60
+
61
+ ## [5.11.2](https://github.com/gotgenes/pi-permission-system/compare/v5.11.1...v5.11.2) (2026-05-08)
62
+
63
+
64
+ ### Documentation
65
+
66
+ * plan removal of legacy path defaults from logging and extension-config ([#113](https://github.com/gotgenes/pi-permission-system/issues/113)) ([27ec0d8](https://github.com/gotgenes/pi-permission-system/commit/27ec0d8668c8c006f94c544e55d10c0eb272ddab))
67
+
68
+
69
+ ### Miscellaneous Chores
70
+
71
+ * approve @google/genai build scripts in pnpm-workspace.yaml ([23b177f](https://github.com/gotgenes/pi-permission-system/commit/23b177f8c5e00d5bc9812fa826c34844cc665d7a))
72
+ * upgrade pnpm to 11.0.8 and update deps ([31eb848](https://github.com/gotgenes/pi-permission-system/commit/31eb848539ff411cb7b6f422cd0244d6c9765ac7))
73
+
8
74
  ## [5.11.1](https://github.com/gotgenes/pi-permission-system/compare/v5.11.0...v5.11.1) (2026-05-08)
9
75
 
10
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "5.11.1",
3
+ "version": "5.14.0",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "files": [
@@ -52,10 +52,10 @@
52
52
  "@earendil-works/pi-tui": "*"
53
53
  },
54
54
  "devDependencies": {
55
- "@biomejs/biome": "^2.4.13",
55
+ "@biomejs/biome": "^2.4.14",
56
56
  "@earendil-works/pi-coding-agent": "^0.74.0",
57
57
  "@earendil-works/pi-tui": "^0.74.0",
58
- "@types/node": "^25.6.0",
58
+ "@types/node": "^25.6.2",
59
59
  "markdownlint-cli2": "^0.22.1",
60
60
  "typescript": "6.0.3",
61
61
  "vitest": "^4.1.5"
@@ -1,11 +1,4 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- renameSync,
6
- unlinkSync,
7
- writeFileSync,
8
- } from "node:fs";
1
+ import { existsSync, mkdirSync } from "node:fs";
9
2
  import { dirname, join } from "node:path";
10
3
  import { fileURLToPath } from "node:url";
11
4
 
@@ -21,17 +14,6 @@ export interface PermissionSystemExtensionConfig {
21
14
  piInfrastructureReadPaths?: string[];
22
15
  }
23
16
 
24
- export interface PermissionSystemConfigLoadResult {
25
- config: PermissionSystemExtensionConfig;
26
- created: boolean;
27
- warning?: string;
28
- }
29
-
30
- export interface PermissionSystemConfigSaveResult {
31
- success: boolean;
32
- error?: string;
33
- }
34
-
35
17
  export const DEFAULT_EXTENSION_CONFIG: PermissionSystemExtensionConfig = {
36
18
  debugLog: false,
37
19
  permissionReviewLog: true,
@@ -43,25 +25,6 @@ export function resolveExtensionRoot(moduleUrl = import.meta.url): string {
43
25
  }
44
26
 
45
27
  export const EXTENSION_ROOT = resolveExtensionRoot();
46
- export const CONFIG_PATH = join(EXTENSION_ROOT, "config.json");
47
- export const LOGS_DIR = join(EXTENSION_ROOT, "logs");
48
- export const DEBUG_LOG_PATH = join(LOGS_DIR, `${EXTENSION_ID}-debug.jsonl`);
49
- export const PERMISSION_REVIEW_LOG_PATH = join(
50
- LOGS_DIR,
51
- `${EXTENSION_ID}-permission-review.jsonl`,
52
- );
53
-
54
- function cloneDefaultConfig(): PermissionSystemExtensionConfig {
55
- return {
56
- debugLog: DEFAULT_EXTENSION_CONFIG.debugLog,
57
- permissionReviewLog: DEFAULT_EXTENSION_CONFIG.permissionReviewLog,
58
- yoloMode: DEFAULT_EXTENSION_CONFIG.yoloMode,
59
- };
60
- }
61
-
62
- function createDefaultConfigContent(): string {
63
- return `${JSON.stringify(DEFAULT_EXTENSION_CONFIG, null, 2)}\n`;
64
- }
65
28
 
66
29
  const PERMISSION_POLICY_KEYS: ReadonlySet<string> = new Set([
67
30
  "defaultPolicy",
@@ -100,108 +63,8 @@ export function normalizePermissionSystemConfig(
100
63
  return result;
101
64
  }
102
65
 
103
- function ensureConfigDirectory(configPath: string): void {
104
- mkdirSync(dirname(configPath), { recursive: true });
105
- }
106
-
107
- export function ensurePermissionSystemConfig(configPath = CONFIG_PATH): {
108
- created: boolean;
109
- warning?: string;
110
- } {
111
- if (existsSync(configPath)) {
112
- return { created: false };
113
- }
114
-
115
- try {
116
- ensureConfigDirectory(configPath);
117
- writeFileSync(configPath, createDefaultConfigContent(), "utf-8");
118
- return { created: true };
119
- } catch (error) {
120
- const message = error instanceof Error ? error.message : String(error);
121
- return {
122
- created: false,
123
- warning: `Failed to initialize permission-system config at '${configPath}': ${message}`,
124
- };
125
- }
126
- }
127
-
128
- export function loadPermissionSystemConfig(
129
- configPath = CONFIG_PATH,
130
- ): PermissionSystemConfigLoadResult {
131
- const ensureResult = ensurePermissionSystemConfig(configPath);
132
-
133
- try {
134
- const raw = readFileSync(configPath, "utf-8");
135
- const parsed = JSON.parse(raw) as unknown;
136
- const config = normalizePermissionSystemConfig(parsed);
137
- const misplacedKeys = detectMisplacedPermissionKeys(toRecord(parsed));
138
-
139
- const warnings: string[] = [];
140
- if (ensureResult.warning) {
141
- warnings.push(ensureResult.warning);
142
- }
143
- if (misplacedKeys.length > 0) {
144
- warnings.push(
145
- `config.json contains legacy permission-rule keys that are ignored: ${misplacedKeys.join(", ")}.\n` +
146
- 'Use the flat permission format: { "permission": { "*": "ask", "read": "allow", ... } }.\n' +
147
- "See config/config.example.json for the new format.",
148
- );
149
- }
150
-
151
- return {
152
- config,
153
- created: ensureResult.created,
154
- warning: warnings.length > 0 ? warnings.join("\n") : undefined,
155
- };
156
- } catch (error) {
157
- const message = error instanceof Error ? error.message : String(error);
158
- return {
159
- config: cloneDefaultConfig(),
160
- created: ensureResult.created,
161
- warning:
162
- ensureResult.warning ??
163
- `Failed to read permission-system config at '${configPath}': ${message}`,
164
- };
165
- }
166
- }
167
-
168
- export function savePermissionSystemConfig(
169
- config: PermissionSystemExtensionConfig,
170
- configPath = CONFIG_PATH,
171
- ): PermissionSystemConfigSaveResult {
172
- const normalized = normalizePermissionSystemConfig(config);
173
- const tmpPath = `${configPath}.tmp`;
174
-
175
- try {
176
- ensureConfigDirectory(configPath);
177
- writeFileSync(tmpPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
178
- renameSync(tmpPath, configPath);
179
- return { success: true };
180
- } catch (error) {
181
- try {
182
- if (existsSync(tmpPath)) {
183
- unlinkSync(tmpPath);
184
- }
185
- } catch {
186
- // Ignore cleanup failures.
187
- }
188
-
189
- const message = error instanceof Error ? error.message : String(error);
190
- return {
191
- success: false,
192
- error: `Failed to save permission-system config at '${configPath}': ${message}`,
193
- };
194
- }
195
- }
196
-
197
- export function getPermissionSystemConfigPath(
198
- configPath = CONFIG_PATH,
199
- ): string {
200
- return configPath;
201
- }
202
-
203
66
  export function ensurePermissionSystemLogsDirectory(
204
- logsDir = LOGS_DIR,
67
+ logsDir: string,
205
68
  ): string | undefined {
206
69
  try {
207
70
  mkdirSync(logsDir, { recursive: true });
package/src/logging.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  import { appendFileSync } from "node:fs";
2
2
 
3
3
  import {
4
- DEBUG_LOG_PATH,
5
4
  EXTENSION_ID,
6
- ensurePermissionSystemLogsDirectory,
7
- LOGS_DIR,
8
- PERMISSION_REVIEW_LOG_PATH,
9
5
  type PermissionSystemExtensionConfig,
10
6
  } from "./extension-config";
11
7
 
@@ -48,19 +44,15 @@ export interface PermissionSystemLogger {
48
44
 
49
45
  interface PermissionSystemLoggerOptions {
50
46
  getConfig: () => PermissionSystemExtensionConfig;
51
- debugLogPath?: string;
52
- reviewLogPath?: string;
53
- ensureLogsDirectory?: () => string | undefined;
47
+ debugLogPath: string;
48
+ reviewLogPath: string;
49
+ ensureLogsDirectory: () => string | undefined;
54
50
  }
55
51
 
56
52
  export function createPermissionSystemLogger(
57
53
  options: PermissionSystemLoggerOptions,
58
54
  ): PermissionSystemLogger {
59
- const debugLogPath = options.debugLogPath ?? DEBUG_LOG_PATH;
60
- const reviewLogPath = options.reviewLogPath ?? PERMISSION_REVIEW_LOG_PATH;
61
- const ensureLogsDirectory =
62
- options.ensureLogsDirectory ??
63
- (() => ensurePermissionSystemLogsDirectory(LOGS_DIR));
55
+ const { debugLogPath, reviewLogPath, ensureLogsDirectory } = options;
64
56
 
65
57
  const writeLine = (
66
58
  stream: "debug" | "review",
@@ -21,11 +21,18 @@ export function compileWildcardPattern<TState>(
21
21
  state: TState,
22
22
  ): CompiledWildcardPattern<TState> {
23
23
  const expanded = expandHomePath(pattern);
24
- const escaped = expanded
24
+ let escaped = expanded
25
25
  .split("*")
26
- .map((part) => escapeRegExp(part))
26
+ .map((part) => escapeRegExp(part).replaceAll("\\?", "."))
27
27
  .join(".*");
28
28
 
29
+ // If the pattern ends with " *" (space + wildcard), make the trailing
30
+ // space-and-arguments portion optional so that e.g. "git *" matches both
31
+ // "git status" and bare "git". Mirrors OpenCode wildcard semantics.
32
+ if (escaped.endsWith(" .*")) {
33
+ escaped = escaped.slice(0, -3) + "( .*)?";
34
+ }
35
+
29
36
  return {
30
37
  pattern,
31
38
  state,
@@ -62,7 +69,8 @@ export function findCompiledWildcardMatch<TState>(
62
69
 
63
70
  /**
64
71
  * Test whether `value` matches `pattern` using wildcard rules.
65
- * `*` in the pattern matches any sequence of characters (including empty).
72
+ * `*` matches any sequence of characters (including empty).
73
+ * `?` matches exactly one character.
66
74
  * Used by evaluate() for rule matching.
67
75
  */
68
76
  export function wildcardMatch(pattern: string, value: string): boolean {
@@ -1,14 +1,13 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { test, vi } from "vitest";
6
6
  import { registerPermissionSystemCommand } from "../src/config-modal";
7
7
  import {
8
8
  DEFAULT_EXTENSION_CONFIG,
9
- loadPermissionSystemConfig,
9
+ normalizePermissionSystemConfig,
10
10
  type PermissionSystemExtensionConfig,
11
- savePermissionSystemConfig,
12
11
  } from "../src/extension-config";
13
12
  import type { Rule } from "../src/rule";
14
13
 
@@ -136,17 +135,28 @@ test("permission-system command handlers manage config summary, persistence, and
136
135
  };
137
136
 
138
137
  try {
139
- const initialSave = savePermissionSystemConfig(config, configPath);
140
- assert.equal(initialSave.success, true);
138
+ writeFileSync(
139
+ configPath,
140
+ `${JSON.stringify(normalizePermissionSystemConfig(config), null, 2)}\n`,
141
+ "utf-8",
142
+ );
141
143
 
142
144
  const controller = {
143
145
  getConfig: () => config,
144
146
  setConfig: (next: PermissionSystemExtensionConfig) => {
145
- const normalized = loadPermissionSystemConfig(configPath).config;
146
- const saved = savePermissionSystemConfig(next, configPath);
147
- assert.equal(saved.success, true);
148
- config = loadPermissionSystemConfig(configPath).config;
149
- assert.notDeepEqual(config, normalized);
147
+ const currentConfig = normalizePermissionSystemConfig(
148
+ JSON.parse(readFileSync(configPath, "utf-8")) as unknown,
149
+ );
150
+ const normalized = normalizePermissionSystemConfig(next);
151
+ writeFileSync(
152
+ configPath,
153
+ `${JSON.stringify(normalized, null, 2)}\n`,
154
+ "utf-8",
155
+ );
156
+ config = normalizePermissionSystemConfig(
157
+ JSON.parse(readFileSync(configPath, "utf-8")) as unknown,
158
+ );
159
+ assert.notDeepEqual(config, currentConfig);
150
160
  },
151
161
  getConfigPath: () => configPath,
152
162
  };
@@ -100,6 +100,7 @@ test("config.resolved entry appears in review log via logger", () => {
100
100
  const logsDir = join(tempDir, "logs");
101
101
  mkdirSync(logsDir, { recursive: true });
102
102
  const reviewLogPath = join(logsDir, "review.jsonl");
103
+ const debugLogPath = join(logsDir, "debug.jsonl");
103
104
 
104
105
  const globalConfigPath = join(tempDir, "pi-permissions.jsonc");
105
106
  writeFileSync(globalConfigPath, "{}", "utf-8");
@@ -116,6 +117,7 @@ test("config.resolved entry appears in review log via logger", () => {
116
117
  permissionReviewLog: true,
117
118
  yoloMode: false,
118
119
  }),
120
+ debugLogPath,
119
121
  reviewLogPath,
120
122
  ensureLogsDirectory: () => undefined,
121
123
  });
@@ -1,11 +1,7 @@
1
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
1
+ import { describe, expect, it } from "vitest";
5
2
 
6
3
  import {
7
4
  detectMisplacedPermissionKeys,
8
- loadPermissionSystemConfig,
9
5
  normalizePermissionSystemConfig,
10
6
  } from "../src/extension-config";
11
7
 
@@ -78,61 +74,6 @@ describe("detectMisplacedPermissionKeys", () => {
78
74
  });
79
75
  });
80
76
 
81
- describe("loadPermissionSystemConfig", () => {
82
- let tempDir: string;
83
- let configPath: string;
84
-
85
- beforeEach(() => {
86
- tempDir = mkdtempSync(join(tmpdir(), "perm-config-test-"));
87
- configPath = join(tempDir, "config.json");
88
- });
89
-
90
- afterEach(() => {
91
- rmSync(tempDir, { recursive: true, force: true });
92
- });
93
-
94
- it("returns no warning for a clean config", () => {
95
- writeFileSync(
96
- configPath,
97
- JSON.stringify({
98
- debugLog: false,
99
- permissionReviewLog: true,
100
- yoloMode: false,
101
- }),
102
- );
103
- const result = loadPermissionSystemConfig(configPath);
104
- expect(result.warning).toBeUndefined();
105
- });
106
-
107
- it("returns a warning naming misplaced permission-rule keys", () => {
108
- writeFileSync(
109
- configPath,
110
- JSON.stringify({
111
- debugLog: true,
112
- defaultPolicy: { tools: "ask" },
113
- bash: { "git status": "allow" },
114
- }),
115
- );
116
- const result = loadPermissionSystemConfig(configPath);
117
- expect(result.warning).toBeDefined();
118
- expect(result.warning).toContain("defaultPolicy");
119
- expect(result.warning).toContain("bash");
120
- expect(result.warning).toContain("permission");
121
- });
122
-
123
- it("still returns the valid extension config fields when misplaced keys are present", () => {
124
- writeFileSync(
125
- configPath,
126
- JSON.stringify({
127
- debugLog: true,
128
- bash: { "git status": "allow" },
129
- }),
130
- );
131
- const result = loadPermissionSystemConfig(configPath);
132
- expect(result.config.debugLog).toBe(true);
133
- });
134
- });
135
-
136
77
  describe("normalizePermissionSystemConfig", () => {
137
78
  it("normalizes a valid config object", () => {
138
79
  const result = normalizePermissionSystemConfig({