@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.
- package/README.md +3 -3
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +24 -0
- package/node_modules/@czottmann/pi-automode/README.md +78 -112
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +18 -14
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +44 -0
- package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +5 -0
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +198 -104
- package/node_modules/@czottmann/pi-automode/docs/configuration.md +155 -0
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +55 -14
- package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +90 -0
- package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +61 -26
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +5 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +172 -18
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +127 -30
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +3 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +273 -31
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +60 -5
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +92 -8
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +203 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +1 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +11 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +9 -1
- package/node_modules/@czottmann/pi-automode/package.json +9 -2
- package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +63 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +3 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +8 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +56 -29
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +15 -13
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-handoff/CHANGELOG.md +29 -0
- package/node_modules/@herbertgao/pi-handoff/README.md +15 -18
- package/node_modules/@herbertgao/pi-handoff/package.json +11 -4
- package/node_modules/@herbertgao/pi-handoff/src/complete-text.ts +33 -0
- package/node_modules/@herbertgao/pi-handoff/src/index.ts +127 -232
- package/node_modules/@herbertgao/pi-handoff/src/session-query.ts +15 -30
- package/node_modules/@herbertgao/pi-mermaid-open/CHANGELOG.md +14 -0
- package/node_modules/@herbertgao/pi-mermaid-open/README.md +21 -4
- package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/herdr-plugin.toml +11 -0
- package/node_modules/@herbertgao/pi-mermaid-open/herdr-plugin/viewer.mjs +305 -0
- package/node_modules/@herbertgao/pi-mermaid-open/package.json +15 -4
- package/node_modules/@herbertgao/pi-mermaid-open/src/index.ts +351 -101
- package/node_modules/@herbertgao/pi-preferred-thinking/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-preferred-thinking/README.md +30 -14
- package/node_modules/@herbertgao/pi-preferred-thinking/package.json +7 -4
- package/node_modules/@herbertgao/pi-preferred-thinking/src/index.ts +140 -85
- package/node_modules/@herbertgao/pi-recap/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-recap/README.md +5 -3
- package/node_modules/@herbertgao/pi-recap/package.json +9 -3
- package/node_modules/@herbertgao/pi-recap/src/index.ts +3 -7
- package/node_modules/@herbertgao/pi-recap/src/model-picker.ts +6 -19
- package/node_modules/@herbertgao/pi-recap/src/models.ts +9 -21
- package/node_modules/@herbertgao/pi-recap/src/tui.ts +1 -1
- package/node_modules/@herbertgao/pi-rename/CHANGELOG.md +49 -0
- package/node_modules/@herbertgao/pi-rename/README.md +13 -11
- package/node_modules/@herbertgao/pi-rename/package.json +13 -3
- package/node_modules/@herbertgao/pi-rename/src/index.ts +135 -59
- package/node_modules/@herbertgao/pi-rename/src/models.ts +5 -13
- package/node_modules/@herbertgao/pi-rename/src/naming.ts +2 -6
- package/node_modules/@herbertgao/pi-stash/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +16 -3
- package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +15 -4
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +16 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +5 -1
- package/node_modules/pi-web-access/CHANGELOG.md +13 -0
- package/node_modules/pi-web-access/README.md +2 -2
- package/node_modules/pi-web-access/chrome-cookies.ts +12 -7
- package/node_modules/pi-web-access/gemini-search.ts +28 -10
- package/node_modules/pi-web-access/index.ts +24 -10
- package/node_modules/pi-web-access/package.json +1 -1
- package/package.json +13 -12
|
@@ -1,35 +1,119 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
13
|
-
|
|
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"
|
|
21
|
-
const
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
return
|
|
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
|
|
298
|
+
if (!pattern.argumentPattern) return true;
|
|
109
299
|
const primary = getPrimaryArgument(toolName, input, cwd);
|
|
110
|
-
return
|
|
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))
|
|
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.
|
|
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.
|
|
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`
|
|
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.
|
|
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" | "
|
|
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: {
|