@herbertgao/pi-extensions 2026.8.8 → 2026.8.9

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.
Files changed (74) hide show
  1. package/README.md +3 -3
  2. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
  3. package/node_modules/@czottmann/pi-automode/README.md +78 -112
  4. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
  5. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
  6. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
  7. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
  8. package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
  9. package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
  10. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
  11. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
  12. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
  13. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
  14. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
  15. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
  16. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
  17. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
  18. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
  19. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
  20. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
  21. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
  22. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
  23. package/node_modules/@czottmann/pi-automode/package.json +9 -2
  24. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
  25. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
  26. package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +15 -13
  31. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  32. package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
  33. package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
  34. package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
  35. package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
  36. package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
  37. package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
  38. package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
  39. package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
  40. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
  41. package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
  42. package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
  43. package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
  44. package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
  45. package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
  46. package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
  47. package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
  48. package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
  49. package/node_modules/@herbertgao/pi-recap/README.md +5 -3
  50. package/node_modules/@herbertgao/pi-recap/package.json +9 -3
  51. package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
  52. package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
  53. package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
  54. package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
  55. package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
  56. package/node_modules/@herbertgao/pi-rename/README.md +13 -11
  57. package/node_modules/@herbertgao/pi-rename/package.json +13 -3
  58. package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
  59. package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
  60. package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
  61. package/node_modules/@herbertgao/pi-stash/package.json +3 -3
  62. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  63. package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
  64. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  65. package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
  66. package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
  67. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
  68. package/node_modules/pi-web-access/CHANGELOG.md +13 -0
  69. package/node_modules/pi-web-access/README.md +2 -2
  70. package/node_modules/pi-web-access/chrome-cookies.ts +12 -7
  71. package/node_modules/pi-web-access/gemini-search.ts +28 -10
  72. package/node_modules/pi-web-access/index.ts +24 -10
  73. package/node_modules/pi-web-access/package.json +1 -1
  74. package/package.json +13 -12
@@ -1,35 +1,119 @@
1
- import { lstatSync, readlinkSync, realpathSync } from "node:fs";
1
+ import {
2
+ accessSync,
3
+ constants,
4
+ lstatSync,
5
+ readlinkSync,
6
+ realpathSync,
7
+ } from "node:fs";
2
8
  import {
3
9
  basename,
4
10
  dirname,
5
11
  isAbsolute,
12
+ join,
6
13
  normalize,
7
14
  relative,
8
15
  resolve,
9
16
  } from "node:path";
17
+ import { fileURLToPath } from "node:url";
10
18
  import { HOME, PATH_BEARING_TOOLS, PROFILE_FILES } from "./constants.ts";
11
19
 
12
- function stripLeadingAt(value: string): string {
13
- return value.startsWith("@") ? value.slice(1) : value;
20
+ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
21
+
22
+ /** Convert Git Bash, MSYS, Cygwin, and WSL drive paths for Windows APIs. */
23
+ function normalizeWindowsShellPath(path: string): string {
24
+ if (
25
+ process.platform !== "win32" ||
26
+ !path.startsWith("/") ||
27
+ path.startsWith("//") ||
28
+ path.includes("\\")
29
+ ) {
30
+ return path;
31
+ }
32
+ const match = path.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
33
+ if (!match) return path;
34
+ const suffix = match[2]?.replaceAll("/", "\\");
35
+ return `${match[1]?.toUpperCase()}:\\${suffix ?? ""}`;
36
+ }
37
+
38
+ /** Mirror Pi's path normalization options. */
39
+ function normalizeInputPath(
40
+ value: string,
41
+ options: { normalizeUnicodeSpaces?: boolean; stripAtPrefix?: boolean } = {},
42
+ ): string {
43
+ let normalized = options.normalizeUnicodeSpaces
44
+ ? value.replace(UNICODE_SPACES, " ")
45
+ : value;
46
+ if (options.stripAtPrefix && normalized.startsWith("@")) {
47
+ normalized = normalized.slice(1);
48
+ }
49
+ normalized = normalizeWindowsShellPath(normalized);
50
+ if (normalized === "~") return HOME;
51
+ if (
52
+ normalized.startsWith("~/") ||
53
+ (process.platform === "win32" && normalized.startsWith("~\\"))
54
+ ) {
55
+ return join(HOME, normalized.slice(2));
56
+ }
57
+ if (/^file:\/\//.test(normalized)) return fileURLToPath(normalized);
58
+ return normalized;
14
59
  }
15
60
 
16
61
  export function resolveInputPath(
17
62
  cwd: string,
18
63
  value: unknown,
19
64
  ): string | undefined {
20
- if (typeof value !== "string" || value.trim() === "") return undefined;
21
- const raw = stripLeadingAt(value.trim());
22
- return isAbsolute(raw) ? resolve(raw) : resolve(cwd, raw);
65
+ if (typeof value !== "string") return undefined;
66
+ const normalized = normalizeInputPath(value, {
67
+ normalizeUnicodeSpaces: true,
68
+ stripAtPrefix: true,
69
+ });
70
+ return isAbsolute(normalized)
71
+ ? resolve(normalized)
72
+ : resolve(normalizeInputPath(cwd), normalized);
23
73
  }
24
74
 
25
- /** The target path a file tool operates on, from `input.path` (or undefined). */
75
+ function existingReadVariant(path: string): string {
76
+ const candidates = [
77
+ path,
78
+ path.replace(/ (AM|PM)\./gi, "\u202F$1."),
79
+ path.normalize("NFD"),
80
+ path.replace(/'/g, "\u2019"),
81
+ path.normalize("NFD").replace(/'/g, "\u2019"),
82
+ ];
83
+ for (const candidate of candidates) {
84
+ try {
85
+ accessSync(candidate, constants.F_OK);
86
+ return candidate;
87
+ } catch {
88
+ // Try the next Pi-compatible read fallback.
89
+ }
90
+ }
91
+ return path;
92
+ }
93
+
94
+ /** Resolve the path that the named Pi file tool will operate on. */
95
+ export function resolveToolInputPath(
96
+ toolName: string,
97
+ cwd: string,
98
+ value: unknown,
99
+ ): string | undefined {
100
+ const resolved = resolveInputPath(cwd, value);
101
+ if (!resolved || toolName !== "read") return resolved;
102
+ return existingReadVariant(resolved);
103
+ }
104
+
105
+ /** The effective target path of a file tool, including Pi's `.` defaults. */
26
106
  export function extractInputPath(
27
107
  toolName: string,
28
108
  input: Record<string, unknown>,
29
109
  ): string | undefined {
30
110
  if (!PATH_BEARING_TOOLS.has(toolName)) return undefined;
31
111
  const value = input.path;
32
- return typeof value === "string" && value.trim() !== "" ? value : undefined;
112
+ if (typeof value === "string" && value !== "") return value;
113
+ if (toolName === "grep" || toolName === "find" || toolName === "ls") {
114
+ return ".";
115
+ }
116
+ return typeof value === "string" ? value : undefined;
33
117
  }
34
118
 
35
119
  /** Expand a leading `~`, `$HOME`, or `${HOME}` in a path-denial pattern. */
@@ -2,9 +2,31 @@ import type { ToolPattern } from "./types.ts";
2
2
  import {
3
3
  expandHomePattern,
4
4
  normalizePathForMatch,
5
- resolveInputPath,
5
+ resolvePathForPolicy,
6
+ resolveToolInputPath,
6
7
  } from "./paths.ts";
7
8
 
9
+ export const MAX_WILDCARD_PATTERN_LENGTH = 4096;
10
+ export const MAX_WILDCARD_INPUT_LENGTH = 1024 * 1024;
11
+
12
+ /** Preserve the previous non-Unicode RegExp `/i` case-equivalence rules. */
13
+ function canonicalizeCase(value: string): string {
14
+ let canonical = "";
15
+ for (let index = 0; index < value.length; index += 1) {
16
+ const character = value[index] ?? "";
17
+ const uppercase = character.toUpperCase();
18
+ if (
19
+ uppercase.length !== 1 ||
20
+ (character.charCodeAt(0) >= 128 && uppercase.charCodeAt(0) < 128)
21
+ ) {
22
+ canonical += character;
23
+ } else {
24
+ canonical += uppercase;
25
+ }
26
+ }
27
+ return canonical;
28
+ }
29
+
8
30
  function normalizeToolName(name: string): string {
9
31
  const lower = name.trim().replace(/^@/, "").toLowerCase();
10
32
  const aliases: Record<string, string> = {
@@ -40,11 +62,108 @@ export function parseToolPattern(value: unknown): ToolPattern | undefined {
40
62
  };
41
63
  }
42
64
 
43
- function wildcardToRegExp(pattern: string): RegExp {
44
- const escaped = pattern
45
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
46
- .replace(/\*/g, ".*");
47
- return new RegExp(`^${escaped}$`, "i");
65
+ function literalPrefixTable(value: string): number[] {
66
+ const table = new Array<number>(value.length).fill(0);
67
+ let prefixLength = 0;
68
+ for (let index = 1; index < value.length; index += 1) {
69
+ while (
70
+ prefixLength > 0 && value[index] !== value[prefixLength]
71
+ ) {
72
+ prefixLength = table[prefixLength - 1] ?? 0;
73
+ }
74
+ if (value[index] === value[prefixLength]) prefixLength += 1;
75
+ table[index] = prefixLength;
76
+ }
77
+ return table;
78
+ }
79
+
80
+ function findLiteral(
81
+ value: string,
82
+ literal: string,
83
+ start: number,
84
+ end: number,
85
+ ): number {
86
+ const prefixTable = literalPrefixTable(literal);
87
+ let matched = 0;
88
+ for (let index = start; index < end; index += 1) {
89
+ while (matched > 0 && value[index] !== literal[matched]) {
90
+ matched = prefixTable[matched - 1] ?? 0;
91
+ }
92
+ if (value[index] === literal[matched]) matched += 1;
93
+ if (matched === literal.length) return index - literal.length + 1;
94
+ }
95
+ return -1;
96
+ }
97
+
98
+ export type WildcardOverflowPolicy = "match" | "no-match";
99
+
100
+ /**
101
+ * Match a case-insensitive `*` wildcard pattern in linear time.
102
+ *
103
+ * `*` matches zero or more characters, including newlines and path separators.
104
+ * Denial callers use `match` for over-limit values so they fail closed. Allow
105
+ * callers use `no-match` so an oversized input cannot broaden an allow rule.
106
+ */
107
+ export function matchesWildcardPattern(
108
+ pattern: string,
109
+ value: string,
110
+ overflowPolicy: WildcardOverflowPolicy = "match",
111
+ ): boolean {
112
+ if (
113
+ pattern.length > MAX_WILDCARD_PATTERN_LENGTH ||
114
+ value.length > MAX_WILDCARD_INPUT_LENGTH
115
+ ) {
116
+ return overflowPolicy === "match";
117
+ }
118
+
119
+ const normalizedPattern = canonicalizeCase(pattern);
120
+ const normalizedValue = canonicalizeCase(value);
121
+ if (!normalizedPattern.includes("*")) {
122
+ return normalizedPattern === normalizedValue;
123
+ }
124
+
125
+ const startsWithWildcard = normalizedPattern.startsWith("*");
126
+ const endsWithWildcard = normalizedPattern.endsWith("*");
127
+ const literals = normalizedPattern.split("*").filter(Boolean);
128
+ if (literals.length === 0) return true;
129
+
130
+ let literalIndex = 0;
131
+ let valueIndex = 0;
132
+ let lastLiteralIndex = literals.length;
133
+
134
+ if (!startsWithWildcard) {
135
+ const prefix = literals[0] ?? "";
136
+ if (!normalizedValue.startsWith(prefix)) return false;
137
+ valueIndex = prefix.length;
138
+ literalIndex = 1;
139
+ }
140
+
141
+ let searchEnd = normalizedValue.length;
142
+ if (!endsWithWildcard) {
143
+ const suffix = literals[literals.length - 1] ?? "";
144
+ searchEnd -= suffix.length;
145
+ if (
146
+ searchEnd < valueIndex ||
147
+ !normalizedValue.endsWith(suffix)
148
+ ) {
149
+ return false;
150
+ }
151
+ lastLiteralIndex -= 1;
152
+ }
153
+
154
+ for (; literalIndex < lastLiteralIndex; literalIndex += 1) {
155
+ const literal = literals[literalIndex] ?? "";
156
+ const found = findLiteral(
157
+ normalizedValue,
158
+ literal,
159
+ valueIndex,
160
+ searchEnd,
161
+ );
162
+ if (found < 0) return false;
163
+ valueIndex = found + literal.length;
164
+ }
165
+
166
+ return true;
48
167
  }
49
168
 
50
169
  function getPrimaryArgument(
@@ -60,7 +179,7 @@ function getPrimaryArgument(
60
179
  typeof input.path === "string"
61
180
  ) {
62
181
  return normalizePathForMatch(
63
- resolveInputPath(cwd, input.path) ?? input.path,
182
+ resolveToolInputPath(toolName, cwd, input.path) ?? input.path,
64
183
  cwd,
65
184
  );
66
185
  }
@@ -72,7 +191,7 @@ function getPrimaryArgument(
72
191
  typeof input.path === "string"
73
192
  ) {
74
193
  return normalizePathForMatch(
75
- resolveInputPath(cwd, input.path) ?? input.path,
194
+ resolveToolInputPath(toolName, cwd, input.path) ?? input.path,
76
195
  cwd,
77
196
  );
78
197
  }
@@ -89,10 +208,80 @@ export function matchesDeniedPath(
89
208
  resolvedPath: string,
90
209
  deniedPaths: string[],
91
210
  ): boolean {
92
- const normalized = resolvedPath.replace(/\\/g, "/");
211
+ const normalized = resolvedPath.replace(/\\/g, "/").normalize("NFC");
212
+ return deniedPaths.some((pattern) =>
213
+ deniedPatternVariants(pattern).some((variant) =>
214
+ matchesWildcardPattern(variant.normalize("NFC"), normalized)
215
+ )
216
+ );
217
+ }
218
+
219
+ function deniedPatternVariants(pattern: string): string[] {
220
+ const expanded = expandHomePattern(pattern).replace(/\\/g, "/");
221
+ const wildcardIndex = expanded.indexOf("*");
222
+ if (wildcardIndex === -1) {
223
+ const canonical = resolvePathForPolicy(expanded)?.replace(/\\/g, "/");
224
+ return canonical && canonical !== expanded
225
+ ? [expanded, canonical]
226
+ : [expanded];
227
+ }
228
+
229
+ const fixedPrefix = expanded.slice(0, wildcardIndex);
230
+ const lastSlash = fixedPrefix.lastIndexOf("/");
231
+ if (lastSlash < 0) return [expanded];
232
+ const fixedScope = fixedPrefix.slice(0, lastSlash) || "/";
233
+ const canonicalScope = resolvePathForPolicy(fixedScope)?.replace(/\\/g, "/");
234
+ if (!canonicalScope || canonicalScope === fixedScope) return [expanded];
235
+ const suffix = expanded.slice(lastSlash).replace(/^\/+/, "");
236
+ const canonicalPattern = canonicalScope === "/"
237
+ ? `/${suffix}`
238
+ : `${withoutTrailingSlash(canonicalScope)}/${suffix}`;
239
+ return canonicalPattern === expanded
240
+ ? [expanded]
241
+ : [expanded, canonicalPattern];
242
+ }
243
+
244
+ function withoutTrailingSlash(path: string): string {
245
+ if (path === "/" || /^[A-Za-z]:\/$/.test(path)) return path;
246
+ return path.replace(/\/+$/, "");
247
+ }
248
+
249
+ function wildcardCanMatchDescendant(root: string, pattern: string): boolean {
250
+ const normalizedRoot = withoutTrailingSlash(
251
+ canonicalizeCase(root.replace(/\\/g, "/").normalize("NFC")),
252
+ );
253
+ const prefix = normalizedRoot === "/" || /^[A-Za-z]:\/$/.test(normalizedRoot)
254
+ ? normalizedRoot
255
+ : `${normalizedRoot}/`;
256
+ const normalizedPattern = canonicalizeCase(pattern.normalize("NFC"));
257
+ const wildcardIndex = normalizedPattern.indexOf("*");
258
+ if (wildcardIndex < 0) {
259
+ return normalizedPattern.length > prefix.length &&
260
+ normalizedPattern.startsWith(prefix);
261
+ }
262
+
263
+ const fixedPrefix = normalizedPattern.slice(0, wildcardIndex);
264
+ return prefix.startsWith(fixedPrefix) || fixedPrefix.startsWith(prefix);
265
+ }
266
+
267
+ /**
268
+ * Whether a recursive search scope can contain a path matched by `deniedPaths`.
269
+ *
270
+ * The check asks whether the wildcard pattern can match any path beginning
271
+ * with the search-root prefix. It does not scan the search tree.
272
+ */
273
+ export function recursiveSearchMayReachDeniedPath(
274
+ resolvedRoot: string,
275
+ deniedPaths: string[],
276
+ ): boolean {
277
+ if (resolvedRoot.length > MAX_WILDCARD_INPUT_LENGTH) {
278
+ return deniedPaths.length > 0;
279
+ }
93
280
  return deniedPaths.some((pattern) => {
94
- const expanded = expandHomePattern(pattern).replace(/\\/g, "/");
95
- return wildcardToRegExp(expanded).test(normalized);
281
+ if (pattern.length > MAX_WILDCARD_PATTERN_LENGTH) return true;
282
+ return deniedPatternVariants(pattern).some((expanded) =>
283
+ wildcardCanMatchDescendant(resolvedRoot, expanded)
284
+ );
96
285
  });
97
286
  }
98
287
 
@@ -102,10 +291,11 @@ export function matchesToolPattern(
102
291
  toolName: string,
103
292
  input: Record<string, unknown>,
104
293
  cwd: string,
294
+ overflowPolicy: WildcardOverflowPolicy = "match",
105
295
  ): boolean {
106
296
  if (!pattern.toolName) return false;
107
297
  if (pattern.toolName !== normalizeToolName(toolName)) return false;
108
- if (!pattern.argumentPattern || pattern.argumentPattern === "*") return true;
298
+ if (!pattern.argumentPattern) return true;
109
299
  const primary = getPrimaryArgument(toolName, input, cwd);
110
- return wildcardToRegExp(pattern.argumentPattern).test(primary);
300
+ return matchesWildcardPattern(pattern.argumentPattern, primary, overflowPolicy);
111
301
  }
@@ -37,6 +37,7 @@ export function statusText(
37
37
  `classifier denied: ${state.classifierDenied}`,
38
38
  `permissions.deny rules: ${config.permissionDeny.length}`,
39
39
  `permissions.ask rules: ${config.permissionAsk.length}`,
40
+ `permissions.allow rules: ${config.permissionAllow.length}`,
40
41
  `environment entries: ${config.environment.length}`,
41
42
  `allow entries: ${config.allow.length}`,
42
43
  `soft_deny entries: ${config.softDeny.length}`,
@@ -44,6 +44,8 @@ export type AutoModeSettings = {
44
44
  classifyReadOnlyTools?: boolean;
45
45
  /** Override the fast-stage completion token budget (default 512). */
46
46
  fastClassifierMaxTokens?: number;
47
+ /** Per-request timeout for classifier completions in milliseconds (default 20000). */
48
+ classifierTimeoutMs?: number;
47
49
  /** When true, file tools whose resolved path is inside the working directory are allowed deterministically (no classifier), and outside-CWD file access is classified. */
48
50
  allowInsideWorkingDirectory?: boolean;
49
51
  /** Path glob patterns (file tools) that are always denied before the classifier. Supports `~` and `*` (matches any characters, including `/`). */
@@ -65,6 +67,11 @@ export type SettingsFile = {
65
67
  permissions?: {
66
68
  deny?: unknown;
67
69
  ask?: unknown;
70
+ /**
71
+ * Deterministic allow tier: matching calls skip the classifier only. Read
72
+ * from user-owned config sources, never shared project config.
73
+ */
74
+ allow?: unknown;
68
75
  };
69
76
  };
70
77
 
@@ -86,6 +93,7 @@ export type EffectiveConfig = {
86
93
  classifierReasoningLevel?: ClassifierReasoningLevel;
87
94
  classifyReadOnlyTools: boolean;
88
95
  fastClassifierMaxTokens: number;
96
+ classifierTimeoutMs: number;
89
97
  allowInsideWorkingDirectory: boolean;
90
98
  deniedPaths: string[];
91
99
  maxUserTranscriptTokens: number;
@@ -97,6 +105,7 @@ export type EffectiveConfig = {
97
105
  hardDeny: string[];
98
106
  permissionDeny: ToolPattern[];
99
107
  permissionAsk: ToolPattern[];
108
+ permissionAllow: ToolPattern[];
100
109
  log: LogConfig;
101
110
  };
102
111
 
@@ -128,6 +137,7 @@ export type DenialRecord = {
128
137
  /** Denial kind plus the deterministic allow fast paths, used for decision log entries. */
129
138
  export type DecisionKind =
130
139
  | DenialRecord["kind"]
140
+ | "permissions.allow"
131
141
  | "read-only"
132
142
  | "inside-working-directory";
133
143
 
@@ -161,6 +171,7 @@ export type ClassifierIo = {
161
171
  prompt: {
162
172
  system: string;
163
173
  context: string;
174
+ action: string;
164
175
  fastInstruction: string;
165
176
  detailedInstruction: string;
166
177
  };
@@ -30,7 +30,15 @@ export function safeJson(value: unknown, maxLength = 4000): string {
30
30
  Math.max(200, Math.floor(maxLength / 4)),
31
31
  );
32
32
  }
33
- if (Array.isArray(current)) return current.slice(0, 30);
33
+ if (Array.isArray(current)) {
34
+ if (current.length <= 30) return current;
35
+ return {
36
+ $truncatedArray: true,
37
+ items: current.slice(0, 30),
38
+ omittedEntries: current.length - 30,
39
+ totalEntries: current.length,
40
+ };
41
+ }
34
42
  if (current && typeof current === "object") {
35
43
  if (seen.has(current)) return "[Circular]";
36
44
  seen.add(current);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@czottmann/pi-automode",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Claude Code-style auto mode guardrail for pi.",
5
5
  "repository": {
6
6
  "url": "https://github.com/czottmann/pi-automode"
@@ -25,20 +25,26 @@
25
25
  "test": "node --import tsx --test tests/*.test.ts"
26
26
  },
27
27
  "files": [
28
+ "CHANGELOG.md",
28
29
  "docs",
29
30
  "extensions",
30
31
  "examples",
32
+ "skills",
31
33
  "README.md"
32
34
  ],
33
35
  "pi": {
34
36
  "extensions": [
35
37
  "./extensions/auto-mode.ts"
38
+ ],
39
+ "skills": [
40
+ "./skills"
36
41
  ]
37
42
  },
38
43
  "peerDependencies": {
39
44
  "@earendil-works/pi-ai": "*",
40
45
  "@earendil-works/pi-coding-agent": "*",
41
- "@earendil-works/pi-tui": "*"
46
+ "@earendil-works/pi-tui": "*",
47
+ "typebox": "*"
42
48
  },
43
49
  "devDependencies": {
44
50
  "@earendil-works/pi-ai": "^0.84.1",
@@ -46,6 +52,7 @@
46
52
  "@earendil-works/pi-tui": "^0.84.1",
47
53
  "@types/node": "^24.0.0",
48
54
  "tsx": "^4.22.4",
55
+ "typebox": "^1.3.10",
49
56
  "typescript": "^5.8.0"
50
57
  }
51
58
  }
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: automode-diagnostics
3
+ description: Diagnose unexpected pi-automode decisions from active state, effective rules, and observability logs. Use when auto mode blocks a safe action, allows an unsafe action, or needs a narrowly scoped rule correction.
4
+ ---
5
+
6
+ # Automode diagnostics
7
+
8
+ Use this skill to investigate a pi-automode decision. Read [Agent diagnostics](../../docs/diagnostics.md), [Classifier flow](../../docs/automode-classifier-flow.md), [Defaults](../../docs/defaults.md), and [Observability logging](../../docs/observability-logging.md) before you change configuration.
9
+
10
+ ## Safety rules
11
+
12
+ - Investigate before you edit configuration.
13
+ - Do not evade a denial with another tool, path, or disabled safety control.
14
+ - Do not weaken a rule only to make one action pass.
15
+ - Do not print credentials, tokens, private keys, signed URLs, or sensitive tool input.
16
+ - Preserve unrelated working-tree and configuration changes.
17
+ - Do not run an unsafe rejected action because auto mode is off.
18
+
19
+ `automode_inspect` is read-only. It cannot change auto mode or configuration. Its output is model-visible and omits denial reasons and action payloads. Treat truncated output as incomplete.
20
+
21
+ ## Investigation
22
+
23
+ 1. Call `automode_inspect` with `status`, `config`, and `denials`.
24
+ 2. Read the reported log path only when observability logging is enabled.
25
+ 3. Use the denial timestamp, enforcement kind, and tool name to locate the relevant decision entry.
26
+ 4. Inspect a decision reason only after you verify that it cannot contain sensitive input.
27
+ 5. Identify the enforcement layer before you propose a change:
28
+ - `permissions.deny` and `permissions.ask` are local rules.
29
+ - `deterministic-hard-deny` is extension code.
30
+ - `read-only` is a local allow.
31
+ - `classifier` is a model decision.
32
+ 6. Do not tune classifier rules to solve a permission or deterministic denial.
33
+
34
+ For a classifier decision, use the effective configuration and the logged classifier metadata when available. Do not dump full prompts or raw responses. They can contain session context and tool input.
35
+
36
+ ## Configuration correction
37
+
38
+ Only change configuration when the user explicitly requests a correction.
39
+
40
+ 1. Explain the proposed narrow change.
41
+ 2. Ask the user to run `/automode off`.
42
+ 3. Wait for confirmation.
43
+ 4. While auto mode is off, edit only the requested automode configuration and related evidence files.
44
+ 5. Validate JSON syntax and the exact diff.
45
+ 6. Ask the user to run `/automode reload` and then `/automode on`.
46
+ 7. Call `automode_inspect` with `status` and `config` to confirm that auto mode is enabled and the new configuration is active.
47
+ 8. Retry an action only when the user requested it and the action is safe.
48
+
49
+ When a correction replaces a rule list, check whether it retains `$defaults`. Keep unrelated protections. Test the corrected non-secret case, the secret-bearing variant, an unapproved destination, and an unrelated hard-deny case.
50
+
51
+ ## Report
52
+
53
+ Report:
54
+
55
+ - the active configuration and log paths;
56
+ - the decision timestamp, ID, and enforcement layer;
57
+ - the evidence for the decision;
58
+ - the proposed or applied narrow change;
59
+ - validation performed and expected regression cases;
60
+ - remaining uncertainty; and
61
+ - whether auto mode is enabled.
62
+
63
+ If the session ends while auto mode is off, tell the user to run `/automode on`.
@@ -39,8 +39,8 @@ Run `/reload` after installation.
39
39
  | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
40
40
  | Claude Code Output | Tool summaries, expand/collapse, rich edit/write diffs, and `on` / `compact` / `off` modes | `/ccstyle` |
41
41
  | Fullscreen mode | Tool card/group click-to-toggle, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
42
- | Settings panel | `Style / Diff / Thinking / Feature` tabs with startup, wheel, and optional feature toggles | `/ccstyle` |
43
- | Context inspection | Usage breakdown and previews for the system prompt, tools, tool results, skills, and messages | `/context` |
42
+ | Settings panel | `Style / Diff / Thinking / UI / Feature` tabs with startup, wheel, and optional feature toggles | `/ccstyle` |
43
+ | Context inspection | Usage breakdown and previews for the system prompt, memory, tools, tool results, skills, and messages | `/context` |
44
44
  | Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
45
45
  | Theme | Bundled CC Dark and CC Light themes | `/theme` |
46
46
 
@@ -101,5 +101,5 @@ bun run typecheck
101
101
 
102
102
  ## Credits
103
103
 
104
- - Derived from [`minuque/pi-cc-extensions` v0.8.64](https://github.com/minuque/pi-cc-extensions/releases/tag/v0.8.64) (`4709081`) under the MIT license.
104
+ - Derived from [`minuque/pi-cc-extensions` v0.8.66](https://github.com/minuque/pi-cc-extensions/releases/tag/v0.8.66) (`e971a39`) under the MIT license.
105
105
  - Rich diffs are adapted from [`MasuRii/pi-tool-display`](https://github.com/MasuRii/pi-tool-display) (MIT). See [`extensions/renderer/tool/diff/ATTRIBUTION.md`](./extensions/renderer/tool/diff/ATTRIBUTION.md).
@@ -39,8 +39,8 @@ pi install npm:@herbertgao/pi-cc-extensions
39
39
  | ---------------------- | ---------------------------------------------------------------------------------------------------------- | ------------- |
40
40
  | Claude Code UI | 工具摘要、折叠展开、rich edit/write diff,以及 `on` / `compact` / `off` 三种模式 | `/ccstyle` |
41
41
  | Fullscreen mode | 工具卡/group 点击展开与收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
42
- | 配置面板 | `Style / Diff / Thinking / Feature` 四页签,含启动头、滚轮步进及可选功能开关 | `/ccstyle` |
43
- | 上下文检查 | 查看上下文占用,并预览 System prompt、Tools、Tool results、Skills 和消息内容 | `/context` |
42
+ | 配置面板 | `Style / Diff / Thinking / UI / Feature` 五页签,含启动头、滚轮步进及可选功能开关 | `/ccstyle` |
43
+ | 上下文检查 | 查看上下文占用,并预览 System prompt、Memory、Tools、Tool results、Skills 和消息内容 | `/context` |
44
44
  | Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
45
45
  | 主题 | 随包提供内置 CC Dark、CC Light 主题 | `/theme` |
46
46
 
@@ -101,5 +101,5 @@ bun run typecheck
101
101
 
102
102
  ## 致谢
103
103
 
104
- - 基于 MIT 许可的 [`minuque/pi-cc-extensions` v0.8.64](https://github.com/minuque/pi-cc-extensions/releases/tag/v0.8.64)(`4709081`)衍生维护。
104
+ - 基于 MIT 许可的 [`minuque/pi-cc-extensions` v0.8.66](https://github.com/minuque/pi-cc-extensions/releases/tag/v0.8.66)(`e971a39`)衍生维护。
105
105
  - Rich diff 改编自 [`MasuRii/pi-tool-display`](https://github.com/MasuRii/pi-tool-display)(MIT);详见 [`extensions/renderer/tool/diff/ATTRIBUTION.md`](./extensions/renderer/tool/diff/ATTRIBUTION.md)。
@@ -180,7 +180,7 @@ function buildNumberInputSubmenu(
180
180
 
181
181
  /** Section tabs for /ccstyle — matches Zentui-style "A / B / C" headers. */
182
182
  type CcstyleSection = {
183
- id: "style" | "editor" | "diff" | "thinking" | "feature";
183
+ id: "style" | "diff" | "thinking" | "ui" | "feature";
184
184
  label: string;
185
185
  items: any[];
186
186
  };
@@ -421,6 +421,7 @@ export async function showCcstylePanel(
421
421
  const onSettingChange = (id: string, value: string) => {
422
422
  const featureToggle = featureToggles[id];
423
423
  if (featureToggle) {
424
+ // SAFETY: featureToggles contains only boolean keys from Config.
424
425
  (config as unknown as Record<string, boolean>)[id] = value === "on";
425
426
  featureToggle.apply(value === "on");
426
427
  saveConfig();
@@ -549,12 +550,15 @@ export async function showCcstylePanel(
549
550
  label: "Thinking",
550
551
  items: [thinkingTitleSetting, thinkingPreviewSetting, thinkingAnimationSetting],
551
552
  },
553
+ {
554
+ id: "ui",
555
+ label: "UI",
556
+ items: [startupHeaderSetting, scrollStepSetting],
557
+ },
552
558
  {
553
559
  id: "feature",
554
560
  label: "Feature",
555
561
  items: [
556
- startupHeaderSetting,
557
- scrollStepSetting,
558
562
  sessionReferenceToggle.setting,
559
563
  subagentAutocompleteToggle.setting,
560
564
  contextCommandToggle.setting,
@@ -588,6 +592,7 @@ export async function showCcstylePanel(
588
592
 
589
593
  /** 数值项:当前选中项有 submenu + values 时,Space 仅循环预设,不打开子面板。 */
590
594
  const cyclePresetInList = (list: InstanceType<typeof SettingsList>): boolean => {
595
+ // SAFETY: SettingsList owns these stable fields used by its built-in input handler.
591
596
  const internal = list as unknown as {
592
597
  submenuComponent: unknown;
593
598
  items: {