@herbertgao/pi-extensions 2026.9.4 → 2026.9.6

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 (146) hide show
  1. package/README.md +3 -15
  2. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  3. package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
  4. package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
  5. package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
  6. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  7. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
  8. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
  10. package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
  11. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
  12. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
  14. package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
  15. package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
  16. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
  21. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  22. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  23. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
  34. package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
  35. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
  36. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  37. package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
  38. package/node_modules/@tifan/pi-copy-response/README.md +2 -2
  39. package/node_modules/@tifan/pi-copy-response/package.json +3 -2
  40. package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
  41. package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
  42. package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
  43. package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
  44. package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
  45. package/node_modules/@tifan/pi-recap/package.json +3 -2
  46. package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
  47. package/node_modules/pi-mcp-adapter/README.md +125 -19
  48. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
  49. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
  50. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
  51. package/node_modules/pi-mcp-adapter/commands.ts +13 -15
  52. package/node_modules/pi-mcp-adapter/config.ts +198 -30
  53. package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
  54. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
  55. package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
  56. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
  57. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
  58. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
  59. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
  60. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
  61. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
  62. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
  63. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
  64. package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
  65. package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
  66. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  67. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
  68. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  69. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
  70. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
  71. package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
  72. package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
  73. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  74. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
  75. package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
  76. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  77. package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
  78. package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
  79. package/node_modules/pi-mcp-adapter/index.ts +827 -164
  80. package/node_modules/pi-mcp-adapter/init.ts +48 -10
  81. package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
  82. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
  83. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
  84. package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
  85. package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
  86. package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
  87. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
  88. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
  89. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
  90. package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
  91. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
  92. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
  93. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
  94. package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
  95. package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
  96. package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
  97. package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
  98. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
  99. package/node_modules/pi-mcp-adapter/package.json +13 -3
  100. package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
  101. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
  102. package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
  103. package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
  104. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
  105. package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
  106. package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
  107. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
  108. package/node_modules/pi-mcp-adapter/state.ts +10 -1
  109. package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
  110. package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
  111. package/node_modules/pi-mcp-adapter/types.ts +57 -4
  112. package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
  113. package/node_modules/pi-mcp-adapter/utils.ts +64 -6
  114. package/package.json +13 -21
  115. package/examples/pi-footer.json +0 -275
  116. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
  117. package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
  118. package/node_modules/@czottmann/pi-automode/README.md +0 -215
  119. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
  120. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
  121. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
  122. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
  123. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
  124. package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
  125. package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
  126. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
  127. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
  128. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
  129. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
  130. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
  131. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
  132. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
  133. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
  134. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
  135. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
  136. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
  137. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
  138. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
  139. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
  140. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
  141. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
  142. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
  143. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
  144. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
  145. package/node_modules/@czottmann/pi-automode/package.json +0 -61
  146. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
@@ -1,904 +0,0 @@
1
- import {
2
- existsSync,
3
- linkSync,
4
- lstatSync,
5
- mkdirSync,
6
- readFileSync,
7
- unlinkSync,
8
- writeFileSync,
9
- } from "node:fs";
10
- import { dirname, resolve } from "node:path";
11
- import {
12
- DEFAULT_ALLOW,
13
- DEFAULT_ALLOW_INSIDE_WORKING_DIRECTORY,
14
- DEFAULT_CLASSIFIER_TIMEOUT_MS,
15
- DEFAULT_CLASSIFY_READ_ONLY_TOOLS,
16
- DEFAULT_DENIED_PATHS,
17
- DEFAULT_ENVIRONMENT,
18
- DEFAULT_FAST_CLASSIFIER_MAX_TOKENS,
19
- DEFAULT_HARD_DENY,
20
- DEFAULT_LOG_CONFIG,
21
- DEFAULT_MAX_TOOL_TRANSCRIPT_TOKENS,
22
- DEFAULT_MAX_USER_TRANSCRIPT_TOKENS,
23
- DEFAULT_PROTECTED_PATHS,
24
- DEFAULT_SOFT_DENY,
25
- MAX_CLASSIFIER_TIMEOUT_MS,
26
- PI_GLOBAL_SETTINGS,
27
- PI_LEGACY_GLOBAL_SETTINGS,
28
- PI_PROJECT_LOCAL_SETTINGS,
29
- PI_PROJECT_SHARED_SETTINGS,
30
- } from "./constants.ts";
31
- import {
32
- MAX_WILDCARD_PATTERN_LENGTH,
33
- parseToolPattern,
34
- } from "./permissions.ts";
35
- import type {
36
- AutoModeSettings,
37
- ClassifierReasoningLevel,
38
- ConfigLoadResult,
39
- EffectiveConfig,
40
- LoadedSettingsFile,
41
- LogConfig,
42
- SettingsFile,
43
- SettingsSources,
44
- ToolPattern,
45
- } from "./types.ts";
46
- import { hasOwn, stringArray } from "./utils.ts";
47
-
48
- export type GlobalConfigPreparation = {
49
- status: "current" | "migrated" | "conflict" | "failed";
50
- activePath: string;
51
- diagnostic?: string;
52
- notification?: string;
53
- writeBlockedReason?: string;
54
- };
55
-
56
- export type PrepareGlobalConfigOptions = {
57
- currentPath?: string;
58
- legacyPath?: string;
59
- moveFile?: (source: string, destination: string) => void;
60
- unlinkFile?: (path: string) => void;
61
- };
62
-
63
- function sameFileIdentity(firstPath: string, secondPath: string): boolean {
64
- try {
65
- const first = lstatSync(firstPath);
66
- const second = lstatSync(secondPath);
67
- return first.dev === second.dev && first.ino === second.ino;
68
- } catch {
69
- return false;
70
- }
71
- }
72
-
73
- function cleanupPublishedDestination(
74
- destination: string,
75
- unlinkFile: (path: string) => void,
76
- originalError: unknown,
77
- ): AggregateError | undefined {
78
- try {
79
- unlinkFile(destination);
80
- return undefined;
81
- } catch (cleanupError) {
82
- return new AggregateError(
83
- [originalError, cleanupError],
84
- `Could not clean up interrupted Auto Mode config migration at ${destination}`,
85
- );
86
- }
87
- }
88
-
89
- function moveFileWithoutOverwrite(
90
- source: string,
91
- destination: string,
92
- unlinkFile: (path: string) => void,
93
- ): void {
94
- linkSync(source, destination);
95
- try {
96
- unlinkFile(source);
97
- } catch (error) {
98
- throw cleanupPublishedDestination(destination, unlinkFile, error) ?? error;
99
- }
100
- }
101
-
102
- function globalConfigFailure(
103
- currentPath: string,
104
- legacyPath: string,
105
- error: unknown,
106
- writeBlockedReason?: string,
107
- ): GlobalConfigPreparation {
108
- const message =
109
- `Could not move Auto Mode config from ${legacyPath} to ${currentPath}: ${
110
- error instanceof Error ? error.message : String(error)
111
- }. Using the legacy config for this session${
112
- writeBlockedReason ? "; global config writes are disabled" : ""
113
- }.`;
114
- return {
115
- status: "failed",
116
- activePath: legacyPath,
117
- diagnostic: message,
118
- notification: message,
119
- writeBlockedReason,
120
- };
121
- }
122
-
123
- function globalConfigConflict(
124
- currentPath: string,
125
- legacyPath: string,
126
- ): GlobalConfigPreparation {
127
- const message =
128
- `Auto Mode config conflict: using ${currentPath}; legacy config ${legacyPath} is ignored and was not changed.`;
129
- return {
130
- status: "conflict",
131
- activePath: currentPath,
132
- diagnostic: message,
133
- notification: message,
134
- };
135
- }
136
-
137
- /** Select one global config path for this runtime and migrate a legacy file when possible. */
138
- export function prepareGlobalConfig(
139
- options: PrepareGlobalConfigOptions = {},
140
- ): GlobalConfigPreparation {
141
- const currentPath = options.currentPath ?? PI_GLOBAL_SETTINGS[0];
142
- const legacyPath = options.legacyPath ?? PI_LEGACY_GLOBAL_SETTINGS;
143
- const currentExists = existsSync(currentPath);
144
- const legacyExists = existsSync(legacyPath);
145
- const unlinkFile = options.unlinkFile ?? unlinkSync;
146
-
147
- if (currentExists && legacyExists) {
148
- if (!sameFileIdentity(currentPath, legacyPath)) {
149
- return globalConfigConflict(currentPath, legacyPath);
150
- }
151
- try {
152
- unlinkFile(legacyPath);
153
- return {
154
- status: "migrated",
155
- activePath: currentPath,
156
- notification:
157
- `Completed interrupted Auto Mode config migration from ${legacyPath} to ${currentPath}.`,
158
- };
159
- } catch (error) {
160
- const cleanupError = cleanupPublishedDestination(
161
- currentPath,
162
- unlinkFile,
163
- error,
164
- );
165
- return globalConfigFailure(
166
- currentPath,
167
- legacyPath,
168
- cleanupError ?? error,
169
- cleanupError?.message,
170
- );
171
- }
172
- }
173
- if (currentExists || !legacyExists) {
174
- return { status: "current", activePath: currentPath };
175
- }
176
-
177
- try {
178
- mkdirSync(dirname(currentPath), { recursive: true });
179
- const moveFile = options.moveFile ??
180
- ((source: string, destination: string) =>
181
- moveFileWithoutOverwrite(source, destination, unlinkFile));
182
- moveFile(legacyPath, currentPath);
183
- return {
184
- status: "migrated",
185
- activePath: currentPath,
186
- notification: `Moved Auto Mode config from ${legacyPath} to ${currentPath}.`,
187
- };
188
- } catch (error) {
189
- if (existsSync(currentPath)) {
190
- if (!sameFileIdentity(currentPath, legacyPath)) {
191
- return globalConfigConflict(currentPath, legacyPath);
192
- }
193
- const cleanupError = cleanupPublishedDestination(
194
- currentPath,
195
- unlinkFile,
196
- error,
197
- );
198
- if (cleanupError) {
199
- return globalConfigFailure(
200
- currentPath,
201
- legacyPath,
202
- cleanupError,
203
- cleanupError.message,
204
- );
205
- }
206
- }
207
- return globalConfigFailure(currentPath, legacyPath, error);
208
- }
209
- }
210
-
211
- function readSettingsFile(path: string): LoadedSettingsFile | undefined {
212
- if (!existsSync(path)) return undefined;
213
- try {
214
- const settings = JSON.parse(readFileSync(path, "utf8")) as SettingsFile;
215
- return {
216
- path,
217
- settings,
218
- diagnostics: validateSettingsFile(settings, path),
219
- };
220
- } catch (error) {
221
- return {
222
- path,
223
- diagnostics: [
224
- `${path}: invalid JSON (${
225
- error instanceof Error ? error.message : String(error)
226
- })`,
227
- ],
228
- };
229
- }
230
- }
231
-
232
- function validateStringArraySetting(
233
- value: unknown,
234
- source: string,
235
- key: string,
236
- diagnostics: string[],
237
- ): void {
238
- if (value === undefined) return;
239
- if (!Array.isArray(value)) {
240
- diagnostics.push(`${source}: ${key} must be an array of strings`);
241
- return;
242
- }
243
- for (const [index, entry] of value.entries()) {
244
- if (typeof entry !== "string" || entry.trim() === "") {
245
- diagnostics.push(
246
- `${source}: ${key}[${index}] must be a non-empty string`,
247
- );
248
- }
249
- }
250
- if (value.length > 0 && !value.includes("$defaults")) {
251
- diagnostics.push(
252
- `${source}: ${key} omits "$defaults" and replaces the built-in ${key} rules`,
253
- );
254
- }
255
- }
256
-
257
- /** Validate config shape and emit human-readable diagnostics for `/automode config`. */
258
- export function validateSettingsFile(
259
- settings: SettingsFile,
260
- source: string,
261
- ): string[] {
262
- const diagnostics: string[] = [];
263
- const root = settings as Record<string, unknown>;
264
- for (const key of Object.keys(root)) {
265
- if (key !== "autoMode" && key !== "permissions") {
266
- diagnostics.push(`${source}: unknown top-level key ${key}`);
267
- }
268
- }
269
-
270
- if (settings.autoMode !== undefined) {
271
- if (
272
- !settings.autoMode ||
273
- typeof settings.autoMode !== "object" ||
274
- Array.isArray(settings.autoMode)
275
- ) {
276
- diagnostics.push(`${source}: autoMode must be an object`);
277
- } else {
278
- const autoMode = settings.autoMode as Record<string, unknown>;
279
- const knownAutoMode = new Set([
280
- "enabled",
281
- "classifierModel",
282
- "classifierReasoningLevel",
283
- "classifierTimeoutMs",
284
- "classifyReadOnlyTools",
285
- "fastClassifierMaxTokens",
286
- "allowInsideWorkingDirectory",
287
- "deniedPaths",
288
- "maxUserTranscriptTokens",
289
- "maxToolTranscriptTokens",
290
- "environment",
291
- "allow",
292
- "protectedPaths",
293
- "soft_deny",
294
- "softDeny",
295
- "hard_deny",
296
- "hardDeny",
297
- "log",
298
- ]);
299
- for (const key of Object.keys(autoMode)) {
300
- if (!knownAutoMode.has(key)) {
301
- diagnostics.push(`${source}: unknown autoMode key ${key}`);
302
- }
303
- }
304
- if (
305
- hasOwn(autoMode, "enabled") && typeof autoMode.enabled !== "boolean"
306
- ) {
307
- diagnostics.push(`${source}: autoMode.enabled must be a boolean`);
308
- }
309
- if (
310
- hasOwn(autoMode, "classifierModel") &&
311
- typeof autoMode.classifierModel !== "string"
312
- ) {
313
- diagnostics.push(
314
- `${source}: autoMode.classifierModel must be a provider/model string`,
315
- );
316
- }
317
- if (
318
- hasOwn(autoMode, "classifierReasoningLevel") &&
319
- !isClassifierReasoningLevel(autoMode.classifierReasoningLevel)
320
- ) {
321
- diagnostics.push(
322
- `${source}: autoMode.classifierReasoningLevel must be one of low, medium, high, xhigh, max`,
323
- );
324
- }
325
- if (
326
- hasOwn(autoMode, "classifierTimeoutMs") &&
327
- (!Number.isInteger(autoMode.classifierTimeoutMs) ||
328
- (autoMode.classifierTimeoutMs as number) < 1000 ||
329
- (autoMode.classifierTimeoutMs as number) > MAX_CLASSIFIER_TIMEOUT_MS)
330
- ) {
331
- diagnostics.push(
332
- `${source}: autoMode.classifierTimeoutMs must be an integer from 1000 through ${MAX_CLASSIFIER_TIMEOUT_MS}`,
333
- );
334
- }
335
- if (
336
- hasOwn(autoMode, "classifyReadOnlyTools") &&
337
- typeof autoMode.classifyReadOnlyTools !== "boolean"
338
- ) {
339
- diagnostics.push(
340
- `${source}: autoMode.classifyReadOnlyTools must be a boolean`,
341
- );
342
- }
343
- if (
344
- hasOwn(autoMode, "fastClassifierMaxTokens") &&
345
- (!Number.isInteger(autoMode.fastClassifierMaxTokens) ||
346
- (autoMode.fastClassifierMaxTokens as number) < 16)
347
- ) {
348
- diagnostics.push(
349
- `${source}: autoMode.fastClassifierMaxTokens must be an integer of at least 16`,
350
- );
351
- }
352
- if (
353
- hasOwn(autoMode, "allowInsideWorkingDirectory") &&
354
- typeof autoMode.allowInsideWorkingDirectory !== "boolean"
355
- ) {
356
- diagnostics.push(
357
- `${source}: autoMode.allowInsideWorkingDirectory must be a boolean`,
358
- );
359
- }
360
- validateDeniedPathsSetting(
361
- autoMode.deniedPaths,
362
- source,
363
- diagnostics,
364
- );
365
- for (
366
- const key of [
367
- "maxUserTranscriptTokens",
368
- "maxToolTranscriptTokens",
369
- ] as const
370
- ) {
371
- if (
372
- hasOwn(autoMode, key) &&
373
- (!Number.isInteger(autoMode[key]) || Number(autoMode[key]) < 32)
374
- ) {
375
- diagnostics.push(
376
- `${source}: autoMode.${key} must be an integer of at least 32`,
377
- );
378
- }
379
- }
380
- validateStringArraySetting(
381
- autoMode.environment,
382
- source,
383
- "autoMode.environment",
384
- diagnostics,
385
- );
386
- validateStringArraySetting(
387
- autoMode.allow,
388
- source,
389
- "autoMode.allow",
390
- diagnostics,
391
- );
392
- validateStringArraySetting(
393
- autoMode.protectedPaths,
394
- source,
395
- "autoMode.protectedPaths",
396
- diagnostics,
397
- );
398
- validateStringArraySetting(
399
- autoMode.soft_deny ?? autoMode.softDeny,
400
- source,
401
- "autoMode.soft_deny",
402
- diagnostics,
403
- );
404
- validateStringArraySetting(
405
- autoMode.hard_deny ?? autoMode.hardDeny,
406
- source,
407
- "autoMode.hard_deny",
408
- diagnostics,
409
- );
410
- if (hasOwn(autoMode, "log")) {
411
- validateLogSetting(autoMode.log, source, diagnostics);
412
- }
413
- }
414
- }
415
-
416
- if (settings.permissions !== undefined) {
417
- if (
418
- !settings.permissions ||
419
- typeof settings.permissions !== "object" ||
420
- Array.isArray(settings.permissions)
421
- ) {
422
- diagnostics.push(`${source}: permissions must be an object`);
423
- } else {
424
- const permissions = settings.permissions as Record<string, unknown>;
425
- for (const key of Object.keys(permissions)) {
426
- if (key !== "deny" && key !== "ask" && key !== "allow") {
427
- diagnostics.push(`${source}: unknown permissions key ${key}`);
428
- }
429
- }
430
- for (const key of ["deny", "ask", "allow"] as const) {
431
- const value = permissions[key];
432
- if (value === undefined) continue;
433
- if (!Array.isArray(value)) {
434
- diagnostics.push(
435
- `${source}: permissions.${key} must be an array of tool patterns`,
436
- );
437
- continue;
438
- }
439
- for (const [index, entry] of value.entries()) {
440
- if (typeof entry !== "string" || !parseToolPattern(entry)) {
441
- diagnostics.push(
442
- `${source}: permissions.${key}[${index}] must be a tool pattern string`,
443
- );
444
- } else if (entry.length > MAX_WILDCARD_PATTERN_LENGTH) {
445
- diagnostics.push(
446
- `${source}: permissions.${key}[${index}] must be at most ${MAX_WILDCARD_PATTERN_LENGTH} characters`,
447
- );
448
- }
449
- }
450
- }
451
- }
452
- }
453
-
454
- return diagnostics;
455
- }
456
-
457
- type RuleAccumulator = {
458
- defaults: string[];
459
- includeDefaults: boolean;
460
- entries: string[];
461
- };
462
-
463
- function createRuleAccumulator(defaults: string[]): RuleAccumulator {
464
- return { defaults, includeDefaults: true, entries: [] };
465
- }
466
-
467
- function applyRuleSetting(
468
- accumulator: RuleAccumulator,
469
- value: unknown,
470
- acceptEntry: (entry: string) => boolean = () => true,
471
- ): void {
472
- const entries = stringArray(value);
473
- if (!entries) return;
474
- // Any entry that stringArray or acceptEntry drops marks the list malformed.
475
- // Fail conservative: keep defaults rather than replace them with a partial list.
476
- let malformed = Array.isArray(value) && value.length !== entries.length;
477
- for (const entry of entries) {
478
- if (entry === "$defaults") continue;
479
- if (acceptEntry(entry)) {
480
- accumulator.entries.push(entry);
481
- } else {
482
- malformed = true;
483
- }
484
- }
485
- accumulator.includeDefaults = entries.includes("$defaults") || malformed;
486
- }
487
-
488
- function finalizeRuleSetting(accumulator: RuleAccumulator): string[] {
489
- const base = accumulator.includeDefaults ? accumulator.defaults : [];
490
- return [...new Set([...base, ...accumulator.entries])];
491
- }
492
-
493
- function validateLogSetting(
494
- value: unknown,
495
- source: string,
496
- diagnostics: string[],
497
- ): void {
498
- if (!value || typeof value !== "object" || Array.isArray(value)) {
499
- diagnostics.push(`${source}: autoMode.log must be an object`);
500
- return;
501
- }
502
- const log = value as Record<string, unknown>;
503
- if (hasOwn(log, "enabled") && typeof log.enabled !== "boolean") {
504
- diagnostics.push(`${source}: autoMode.log.enabled must be a boolean`);
505
- }
506
- if (
507
- hasOwn(log, "classifierIo") && typeof log.classifierIo !== "boolean"
508
- ) {
509
- diagnostics.push(`${source}: autoMode.log.classifierIo must be a boolean`);
510
- }
511
- }
512
-
513
- function mergeLog(
514
- base: LogConfig,
515
- patch: Partial<LogConfig> | undefined,
516
- ): LogConfig {
517
- if (!patch) return base;
518
- return {
519
- enabled: typeof patch.enabled === "boolean" ? patch.enabled : base.enabled,
520
- classifierIo: typeof patch.classifierIo === "boolean" ? patch.classifierIo : base.classifierIo,
521
- };
522
- }
523
-
524
- /**
525
- * Validate `deniedPaths`: an array of non-empty path patterns. Unlike the
526
- * `$defaults` rule lists there is no built-in default list, so `$defaults` is
527
- * a no-op (accepted for consistency with the other rule lists) and omitting
528
- * it is not a diagnostic.
529
- */
530
- function validateDeniedPathsSetting(
531
- value: unknown,
532
- source: string,
533
- diagnostics: string[],
534
- ): void {
535
- if (value === undefined) return;
536
- if (!Array.isArray(value)) {
537
- diagnostics.push(`${source}: deniedPaths must be an array of strings`);
538
- return;
539
- }
540
- for (const [index, entry] of value.entries()) {
541
- if (entry === "$defaults") continue;
542
- if (typeof entry !== "string" || entry.trim() === "") {
543
- diagnostics.push(
544
- `${source}: deniedPaths[${index}] must be a non-empty path pattern`,
545
- );
546
- continue;
547
- }
548
- if (entry.length > MAX_WILDCARD_PATTERN_LENGTH) {
549
- diagnostics.push(
550
- `${source}: deniedPaths[${index}] must be at most ${MAX_WILDCARD_PATTERN_LENGTH} characters`,
551
- );
552
- continue;
553
- }
554
- if (!DENIED_PATH_PATTERN_PREFIX.test(entry)) {
555
- diagnostics.push(
556
- `${source}: deniedPaths[${index}] "${entry}" can never match a resolved absolute path; start it with *, ~, $HOME, \${HOME}, or / (e.g. "**/${entry}")`,
557
- );
558
- }
559
- }
560
- }
561
-
562
- /**
563
- * A pattern can only match a resolved absolute path when it starts with a
564
- * form that anchors it: a leading `/`, a home expansion (`~`, `$HOME`,
565
- * `${HOME}`), or a `*` wildcard that absorbs the leading slash. Anything else
566
- * (e.g. `config.json` or `src/secret.txt`) matches only against the bare
567
- * relative name, which the matcher never sees.
568
- */
569
- const DENIED_PATH_PATTERN_PREFIX =
570
- /^(?:\/|~(?:\/|$)|\$HOME(?:\/|$)|\$\{HOME\}(?:\/|$)|\*)/;
571
-
572
- const CLASSIFIER_REASONING_LEVELS = new Set<ClassifierReasoningLevel>([
573
- "low",
574
- "medium",
575
- "high",
576
- "xhigh",
577
- "max",
578
- ]);
579
-
580
- export function isClassifierReasoningLevel(
581
- value: unknown,
582
- ): value is ClassifierReasoningLevel {
583
- return typeof value === "string" &&
584
- CLASSIFIER_REASONING_LEVELS.has(value as ClassifierReasoningLevel);
585
- }
586
-
587
- function validTranscriptBudget(value: unknown): value is number {
588
- return Number.isInteger(value) && Number(value) >= 32;
589
- }
590
-
591
- function validFastClassifierBudget(value: unknown): value is number {
592
- return Number.isInteger(value) && Number(value) >= 16;
593
- }
594
-
595
- function validClassifierTimeout(value: unknown): value is number {
596
- return Number.isInteger(value) &&
597
- Number(value) >= 1000 &&
598
- Number(value) <= MAX_CLASSIFIER_TIMEOUT_MS;
599
- }
600
-
601
- function applyAutoModeScalars(
602
- base: EffectiveConfig,
603
- settings: AutoModeSettings | undefined,
604
- ): EffectiveConfig {
605
- if (!settings) return base;
606
- return {
607
- ...base,
608
- enabled: typeof settings.enabled === "boolean" ? settings.enabled : base.enabled,
609
- classifierModel: settings.classifierModel ?? base.classifierModel,
610
- classifierReasoningLevel: isClassifierReasoningLevel(
611
- settings.classifierReasoningLevel,
612
- )
613
- ? settings.classifierReasoningLevel
614
- : base.classifierReasoningLevel,
615
- classifyReadOnlyTools: typeof settings.classifyReadOnlyTools === "boolean"
616
- ? settings.classifyReadOnlyTools
617
- : base.classifyReadOnlyTools,
618
- allowInsideWorkingDirectory:
619
- typeof settings.allowInsideWorkingDirectory === "boolean"
620
- ? settings.allowInsideWorkingDirectory
621
- : base.allowInsideWorkingDirectory,
622
- fastClassifierMaxTokens: validFastClassifierBudget(
623
- settings.fastClassifierMaxTokens,
624
- )
625
- ? settings.fastClassifierMaxTokens
626
- : base.fastClassifierMaxTokens,
627
- classifierTimeoutMs: validClassifierTimeout(settings.classifierTimeoutMs)
628
- ? settings.classifierTimeoutMs
629
- : base.classifierTimeoutMs,
630
- maxUserTranscriptTokens: validTranscriptBudget(
631
- settings.maxUserTranscriptTokens,
632
- )
633
- ? settings.maxUserTranscriptTokens
634
- : base.maxUserTranscriptTokens,
635
- maxToolTranscriptTokens: validTranscriptBudget(
636
- settings.maxToolTranscriptTokens,
637
- )
638
- ? settings.maxToolTranscriptTokens
639
- : base.maxToolTranscriptTokens,
640
- log: mergeLog(base.log, settings.log),
641
- };
642
- }
643
-
644
- function appendPermissionPatterns(
645
- target: ToolPattern[],
646
- settings: SettingsFile | undefined,
647
- key: "deny" | "ask" | "allow",
648
- ): void {
649
- const values = stringArray(settings?.permissions?.[key]);
650
- if (!values) return;
651
- for (const value of values) {
652
- if (value.length > MAX_WILDCARD_PATTERN_LENGTH) continue;
653
- const pattern = parseToolPattern(value);
654
- if (pattern) target.push(pattern);
655
- }
656
- }
657
-
658
- /**
659
- * Merge settings with Claude Code-style precedence using Pi-owned config files.
660
- *
661
- * Important details:
662
- * - shared project `.pi/automode.json` contributes `permissions.deny` and
663
- * `permissions.ask` but not `permissions.allow` or `autoMode`, so checked-in
664
- * config can only add permission barriers;
665
- * - global, project-local, and inline `autoMode` settings combine additively across scopes;
666
- * - omitting `$defaults` in any scope for a rule list means "replace built-ins" for that list.
667
- */
668
- export function buildEffectiveConfigFromSources(
669
- sources: SettingsSources = {},
670
- ): EffectiveConfig {
671
- let config: EffectiveConfig = {
672
- enabled: true,
673
- classifyReadOnlyTools: DEFAULT_CLASSIFY_READ_ONLY_TOOLS,
674
- allowInsideWorkingDirectory: DEFAULT_ALLOW_INSIDE_WORKING_DIRECTORY,
675
- deniedPaths: [...DEFAULT_DENIED_PATHS],
676
- fastClassifierMaxTokens: DEFAULT_FAST_CLASSIFIER_MAX_TOKENS,
677
- classifierTimeoutMs: DEFAULT_CLASSIFIER_TIMEOUT_MS,
678
- maxUserTranscriptTokens: DEFAULT_MAX_USER_TRANSCRIPT_TOKENS,
679
- maxToolTranscriptTokens: DEFAULT_MAX_TOOL_TRANSCRIPT_TOKENS,
680
- environment: [...DEFAULT_ENVIRONMENT],
681
- allow: [...DEFAULT_ALLOW],
682
- protectedPaths: [...DEFAULT_PROTECTED_PATHS],
683
- softDeny: [...DEFAULT_SOFT_DENY],
684
- hardDeny: [...DEFAULT_HARD_DENY],
685
- permissionDeny: [],
686
- permissionAsk: [],
687
- permissionAllow: [],
688
- log: { ...DEFAULT_LOG_CONFIG },
689
- };
690
-
691
- const globalSettings = sources.globalSettings ?? [];
692
- const projectLocalSettings = sources.projectLocalSettings ?? [];
693
- const projectSharedSettings = sources.projectSharedSettings ?? [];
694
- const inlineSettings = sources.inlineSettings ?? [];
695
-
696
- const configurableSettings = [
697
- ...globalSettings,
698
- ...projectLocalSettings,
699
- ...inlineSettings,
700
- ];
701
- const environment = createRuleAccumulator(DEFAULT_ENVIRONMENT);
702
- const allow = createRuleAccumulator(DEFAULT_ALLOW);
703
- const protectedPaths = createRuleAccumulator(DEFAULT_PROTECTED_PATHS);
704
- const deniedPaths = createRuleAccumulator(DEFAULT_DENIED_PATHS);
705
- const softDeny = createRuleAccumulator(DEFAULT_SOFT_DENY);
706
- const hardDeny = createRuleAccumulator(DEFAULT_HARD_DENY);
707
-
708
- for (const settings of configurableSettings) {
709
- config = applyAutoModeScalars(config, settings.autoMode);
710
- applyRuleSetting(environment, settings.autoMode?.environment);
711
- applyRuleSetting(allow, settings.autoMode?.allow);
712
- applyRuleSetting(protectedPaths, settings.autoMode?.protectedPaths);
713
- applyRuleSetting(
714
- deniedPaths,
715
- settings.autoMode?.deniedPaths,
716
- (entry) => entry.length <= MAX_WILDCARD_PATTERN_LENGTH,
717
- );
718
- applyRuleSetting(
719
- softDeny,
720
- settings.autoMode?.soft_deny ?? settings.autoMode?.softDeny,
721
- );
722
- applyRuleSetting(
723
- hardDeny,
724
- settings.autoMode?.hard_deny ?? settings.autoMode?.hardDeny,
725
- );
726
- }
727
-
728
- config = {
729
- ...config,
730
- environment: finalizeRuleSetting(environment),
731
- allow: finalizeRuleSetting(allow),
732
- protectedPaths: finalizeRuleSetting(protectedPaths),
733
- deniedPaths: finalizeRuleSetting(deniedPaths),
734
- softDeny: finalizeRuleSetting(softDeny),
735
- hardDeny: finalizeRuleSetting(hardDeny),
736
- };
737
-
738
- for (
739
- const settings of [
740
- ...globalSettings,
741
- ...projectSharedSettings,
742
- ...projectLocalSettings,
743
- ...inlineSettings,
744
- ]
745
- ) {
746
- appendPermissionPatterns(config.permissionDeny, settings, "deny");
747
- appendPermissionPatterns(config.permissionAsk, settings, "ask");
748
- }
749
- for (
750
- const settings of [
751
- ...globalSettings,
752
- ...projectLocalSettings,
753
- ...inlineSettings,
754
- ]
755
- ) {
756
- appendPermissionPatterns(config.permissionAllow, settings, "allow");
757
- }
758
-
759
- return config;
760
- }
761
-
762
- function loadedSettingsToSettings(
763
- files: Array<LoadedSettingsFile | undefined>,
764
- ): SettingsFile[] {
765
- return files.flatMap((file) => (file?.settings ? [file.settings] : []));
766
- }
767
-
768
- function loadedSettingsDiagnostics(
769
- files: Array<LoadedSettingsFile | undefined>,
770
- ): string[] {
771
- return files.flatMap((file) => file?.diagnostics ?? []);
772
- }
773
-
774
- function ignoredSharedAllowDiagnostics(
775
- files: Array<LoadedSettingsFile | undefined>,
776
- ): string[] {
777
- return files.flatMap((file) => {
778
- const permissions = file?.settings?.permissions;
779
- if (!file || !permissions || !hasOwn(permissions, "allow")) return [];
780
- return [
781
- `${file.path}: permissions.allow is ignored in shared project config. Use a user-owned config source instead`,
782
- ];
783
- });
784
- }
785
-
786
- /** Load config from disk and environment variables, including diagnostics for `/automode config`. Project files require explicit trust. */
787
- export function loadEffectiveConfigWithDiagnostics(
788
- cwd: string,
789
- projectTrusted = false,
790
- globalSettingsPath = PI_GLOBAL_SETTINGS[0],
791
- ): ConfigLoadResult {
792
- const inlineSettings: SettingsFile[] = [];
793
- const diagnostics: string[] = [];
794
- if (process.env.PI_AUTOMODE_SETTINGS_JSON) {
795
- try {
796
- const parsed = JSON.parse(
797
- process.env.PI_AUTOMODE_SETTINGS_JSON,
798
- ) as SettingsFile;
799
- inlineSettings.push(parsed);
800
- diagnostics.push(
801
- ...validateSettingsFile(parsed, "PI_AUTOMODE_SETTINGS_JSON"),
802
- );
803
- } catch (error) {
804
- diagnostics.push(
805
- `PI_AUTOMODE_SETTINGS_JSON: invalid JSON (${
806
- error instanceof Error ? error.message : String(error)
807
- })`,
808
- );
809
- }
810
- }
811
-
812
- const globalFiles = [readSettingsFile(globalSettingsPath)];
813
- const projectLocalPaths = PI_PROJECT_LOCAL_SETTINGS.map((file) =>
814
- resolve(cwd, file)
815
- );
816
- const projectSharedPaths = PI_PROJECT_SHARED_SETTINGS.map((file) =>
817
- resolve(cwd, file)
818
- );
819
- const projectLocalFiles = projectTrusted
820
- ? projectLocalPaths.map(readSettingsFile)
821
- : [];
822
- const projectSharedFiles = projectTrusted
823
- ? projectSharedPaths.map(readSettingsFile)
824
- : [];
825
- if (!projectTrusted) {
826
- for (
827
- const path of [...projectLocalPaths, ...projectSharedPaths].filter(
828
- existsSync,
829
- )
830
- ) {
831
- diagnostics.push(`${path}: ignored because project is not trusted`);
832
- }
833
- }
834
- const fileDiagnostics = loadedSettingsDiagnostics([
835
- ...globalFiles,
836
- ...projectLocalFiles,
837
- ...projectSharedFiles,
838
- ]);
839
- const sharedAllowDiagnostics = ignoredSharedAllowDiagnostics(
840
- projectSharedFiles,
841
- );
842
-
843
- return {
844
- config: buildEffectiveConfigFromSources({
845
- globalSettings: loadedSettingsToSettings(globalFiles),
846
- projectLocalSettings: loadedSettingsToSettings(projectLocalFiles),
847
- projectSharedSettings: loadedSettingsToSettings(projectSharedFiles),
848
- inlineSettings,
849
- }),
850
- diagnostics: [
851
- ...fileDiagnostics,
852
- ...sharedAllowDiagnostics,
853
- ...diagnostics,
854
- ],
855
- };
856
- }
857
-
858
- /** Load config from disk and environment variables. Exported for tests and diagnostics. */
859
- export function loadEffectiveConfig(
860
- cwd: string,
861
- projectTrusted = false,
862
- globalSettingsPath = PI_GLOBAL_SETTINGS[0],
863
- ): EffectiveConfig {
864
- return loadEffectiveConfigWithDiagnostics(
865
- cwd,
866
- projectTrusted,
867
- globalSettingsPath,
868
- ).config;
869
- }
870
-
871
- function readWritableSettingsFile(path: string): SettingsFile {
872
- if (!existsSync(path)) return {};
873
- const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
874
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
875
- throw new Error(`${path}: root must be a JSON object`);
876
- }
877
- const settings = parsed as SettingsFile;
878
- if (
879
- settings.autoMode !== undefined &&
880
- (!settings.autoMode ||
881
- typeof settings.autoMode !== "object" ||
882
- Array.isArray(settings.autoMode))
883
- ) {
884
- throw new Error(`${path}: autoMode must be a JSON object`);
885
- }
886
- return settings;
887
- }
888
-
889
- /** Persist the global default classifier model while preserving other settings. */
890
- export function writeGlobalClassifierModel(
891
- classifierModel: string,
892
- path = PI_GLOBAL_SETTINGS[0],
893
- ): void {
894
- const settings = readWritableSettingsFile(path);
895
- const next: SettingsFile = {
896
- ...settings,
897
- autoMode: {
898
- ...settings.autoMode,
899
- classifierModel,
900
- },
901
- };
902
- mkdirSync(dirname(path), { recursive: true });
903
- writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
904
- }