@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,113 +0,0 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import {
3
- fuzzyFilter,
4
- Input,
5
- matchesKey,
6
- SelectList,
7
- } from "@earendil-works/pi-tui";
8
- import type { SelectItem } from "@earendil-works/pi-tui";
9
- import { formatModelSpec } from "./model.ts";
10
-
11
- /** Interactive model selector shown when `/automode model` is run without arguments. */
12
- export function promptForClassifierModel(
13
- ctx: ExtensionContext,
14
- current?: string,
15
- ): Promise<string | undefined> {
16
- if (!ctx.hasUI) {
17
- return Promise.resolve(undefined);
18
- }
19
- const available = ctx.modelRegistry.getAvailable();
20
- if (available.length === 0) {
21
- return Promise.resolve(undefined);
22
- }
23
-
24
- const items: SelectItem[] = available.map((model) => {
25
- const spec = formatModelSpec(model);
26
- return {
27
- value: spec,
28
- label: `${model.id} \u001b[2m[${model.provider}]\u001b[0m`,
29
- description: spec === current ? "\u2713" : undefined,
30
- };
31
- });
32
- items.sort((a, b) => a.label.localeCompare(b.label));
33
-
34
- return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => {
35
- const filterInput = new Input();
36
- filterInput.onEscape = () => done(undefined);
37
-
38
- let filtered: SelectItem[] = items;
39
- let selectList = buildModelList(filtered, theme, filterInput, done, tui);
40
-
41
- function applyFilter(query: string): void {
42
- filtered = query
43
- ? fuzzyFilter(items, query, (item) => `${item.label} ${item.value}`)
44
- : items;
45
- selectList = buildModelList(filtered, theme, filterInput, done, tui);
46
- tui.requestRender();
47
- }
48
-
49
- return {
50
- render(width: number) {
51
- const selected = selectList.getSelectedItem();
52
- const lines: string[] = [];
53
- lines.push(theme.fg("accent", theme.bold("Select classifier model")));
54
- lines.push(
55
- theme.fg(
56
- "dim",
57
- "Only showing models from configured providers. Use /login to add providers.",
58
- ),
59
- );
60
- lines.push("");
61
- lines.push(filterInput.render(width).join("\n"));
62
- lines.push("");
63
- lines.push(...selectList.render(width));
64
- lines.push("");
65
- if (selected) {
66
- lines.push(theme.fg("muted", `Model Name: ${selected.label}`));
67
- }
68
- return lines;
69
- },
70
- invalidate() {
71
- /* no-op */
72
- },
73
- handleInput(data: string) {
74
- if (
75
- matchesKey(data, "up") || matchesKey(data, "down") ||
76
- matchesKey(data, "return") || matchesKey(data, "escape")
77
- ) {
78
- selectList.handleInput(data);
79
- tui.requestRender();
80
- return;
81
- }
82
- filterInput.handleInput(data);
83
- applyFilter(filterInput.getValue());
84
- },
85
- };
86
- });
87
- }
88
-
89
- function buildModelList(
90
- items: SelectItem[],
91
- theme: any,
92
- filterInput: Input,
93
- done: (value: string | undefined) => void,
94
- tui: any,
95
- ): SelectList {
96
- const maxVisible = Math.min(10, Math.max(1, items.length));
97
- const list = new SelectList(items, maxVisible, {
98
- selectedPrefix: (text) => theme.fg("accent", text),
99
- selectedText: (text) => theme.fg("accent", text),
100
- description: (text) => theme.fg("muted", text),
101
- scrollInfo: (text) => theme.fg("dim", text),
102
- noMatch: (text) => theme.fg("warning", text),
103
- });
104
- list.setSelectedIndex(0);
105
- list.onCancel = () => done(undefined);
106
- list.onSelect = (item) => done(item.value);
107
- list.onSelectionChange = () => tui.requestRender();
108
- filterInput.onSubmit = () => {
109
- const selected = list.getSelectedItem();
110
- if (selected) done(selected.value);
111
- };
112
- return list;
113
- }
@@ -1,13 +0,0 @@
1
- import type { Model } from "@earendil-works/pi-ai";
2
-
3
- export function parseModelSpec(
4
- spec: string,
5
- ): { provider: string; id: string } | undefined {
6
- const slash = spec.indexOf("/");
7
- if (slash <= 0 || slash >= spec.length - 1) return undefined;
8
- return { provider: spec.slice(0, slash), id: spec.slice(slash + 1) };
9
- }
10
-
11
- export function formatModelSpec(model: Model<any>): string {
12
- return `${model.provider}/${model.id}`;
13
- }
@@ -1,297 +0,0 @@
1
- import {
2
- accessSync,
3
- constants,
4
- lstatSync,
5
- readlinkSync,
6
- realpathSync,
7
- } from "node:fs";
8
- import {
9
- basename,
10
- dirname,
11
- isAbsolute,
12
- join,
13
- normalize,
14
- relative,
15
- resolve,
16
- } from "node:path";
17
- import { fileURLToPath } from "node:url";
18
- import { HOME, PATH_BEARING_TOOLS, PROFILE_FILES } from "./constants.ts";
19
-
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;
59
- }
60
-
61
- export function resolveInputPath(
62
- cwd: string,
63
- value: unknown,
64
- ): string | undefined {
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);
73
- }
74
-
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. */
106
- export function extractInputPath(
107
- toolName: string,
108
- input: Record<string, unknown>,
109
- ): string | undefined {
110
- if (!PATH_BEARING_TOOLS.has(toolName)) return undefined;
111
- const value = input.path;
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;
117
- }
118
-
119
- /** Expand a leading `~`, `$HOME`, or `${HOME}` in a path-denial pattern. */
120
- export function expandHomePattern(pattern: string): string {
121
- const home = HOME.replace(/\\/g, "/");
122
- if (pattern === "~" || pattern === "$HOME" || pattern === "${HOME}") {
123
- return home;
124
- }
125
- if (pattern.startsWith("~/")) return `${home}/${pattern.slice(2)}`;
126
- if (pattern.startsWith("$HOME/")) return `${home}/${pattern.slice(6)}`;
127
- if (pattern.startsWith("${HOME}/")) return `${home}/${pattern.slice(8)}`;
128
- return pattern;
129
- }
130
-
131
- export function normalizePathForMatch(path: string, cwd: string): string {
132
- const normalized = normalize(path);
133
- const rel = relative(cwd, normalized);
134
- return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : normalized;
135
- }
136
-
137
- export function isInside(child: string, parent: string): boolean {
138
- const rel = relative(parent, child);
139
- return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
140
- }
141
-
142
- /** Resolve symlinks through the nearest existing ancestor of a path. */
143
- export function resolvePathForPolicy(path: string): string | undefined {
144
- return resolvePathForPolicyInner(resolve(path), new Set<string>());
145
- }
146
-
147
- function resolvePathForPolicyInner(
148
- path: string,
149
- visitedSymlinks: Set<string>,
150
- ): string | undefined {
151
- let current = path;
152
- const missingSegments: string[] = [];
153
-
154
- while (true) {
155
- try {
156
- return resolve(realpathSync(current), ...missingSegments);
157
- } catch {
158
- try {
159
- const stat = lstatSync(current);
160
- if (!stat.isSymbolicLink() || visitedSymlinks.has(current)) {
161
- return undefined;
162
- }
163
- visitedSymlinks.add(current);
164
- const target = resolve(dirname(current), readlinkSync(current));
165
- return resolvePathForPolicyInner(
166
- resolve(target, ...missingSegments),
167
- visitedSymlinks,
168
- );
169
- } catch (error) {
170
- const code = error && typeof error === "object" && "code" in error
171
- ? String(error.code)
172
- : undefined;
173
- if (code !== "ENOENT" && code !== "ENOTDIR") return undefined;
174
- const parent = dirname(current);
175
- if (parent === current) return undefined;
176
- missingSegments.unshift(basename(current));
177
- current = parent;
178
- }
179
- }
180
- }
181
- }
182
-
183
- function normalizeProtectedPathForMatch(value: string): string {
184
- return value
185
- .replace(/\\/g, "/")
186
- .normalize("NFC")
187
- .toLowerCase()
188
- .normalize("NFC");
189
- }
190
-
191
- export function matchesProtectedPath(
192
- relativePath: string,
193
- protectedPaths: string[],
194
- ): boolean {
195
- const normalizedPath = normalizeProtectedPathForMatch(relativePath);
196
- return protectedPaths.some((pattern) => {
197
- const normalizedPattern = normalizeProtectedPathForMatch(pattern);
198
- return normalizedPath === normalizedPattern ||
199
- normalizedPath.startsWith(`${normalizedPattern}/`);
200
- });
201
- }
202
-
203
- export function isProtectedPath(
204
- path: string,
205
- cwd: string,
206
- protectedPaths: string[],
207
- ): boolean {
208
- // Resolve through the nearest existing ancestor so symlinked directories are
209
- // respected even when the final write target does not exist yet.
210
- const resolved = resolvePathForPolicy(path) ?? path;
211
- const resolvedCwd = resolvePathForPolicy(cwd) ?? cwd;
212
-
213
- // For paths inside the project: use relative path for matching.
214
- if (isInside(resolved, resolvedCwd)) {
215
- return matchesProtectedPath(
216
- relative(resolvedCwd, resolved),
217
- protectedPaths,
218
- );
219
- }
220
-
221
- // For paths outside the project: check every path component suffix.
222
- // This catches writes like ../other-project/.git/config even when cwd
223
- // doesn't contain the target.
224
- const normalizedResolved = resolved.replace(/\\/g, "/");
225
- const segments = normalizedResolved.split("/").filter(Boolean);
226
- for (let i = 0; i < segments.length; i++) {
227
- if (matchesProtectedPath(segments.slice(i).join("/"), protectedPaths)) {
228
- return true;
229
- }
230
- }
231
- return false;
232
- }
233
-
234
- export function isSafetyControlPath(path: string, cwd: string): boolean {
235
- const policyPath = resolvePathForPolicy(path) ?? resolve(path);
236
- const policyCwd = resolvePathForPolicy(cwd) ?? resolve(cwd);
237
- const normalized = normalizeProtectedPathForMatch(policyPath);
238
- const file = basename(normalized);
239
- const piAgentRoot = normalizeProtectedPathForMatch(
240
- resolve(HOME, ".pi/agent"),
241
- );
242
- const globalExtensions = `${piAgentRoot}/extensions`;
243
- const globalSettings = `${piAgentRoot}/settings`;
244
- if (
245
- normalized === `${piAgentRoot}/settings.json` ||
246
- normalized === globalExtensions ||
247
- normalized.startsWith(`${globalExtensions}/`) ||
248
- normalized === globalSettings ||
249
- normalized.startsWith(`${globalSettings}/`)
250
- ) {
251
- return true;
252
- }
253
- if (
254
- normalized.endsWith("/.pi/auto-mode.json") ||
255
- normalized.endsWith("/auto-mode.json")
256
- ) {
257
- return true;
258
- }
259
- if (normalized.includes("/.pi/extensions/") && file.includes("auto")) {
260
- return true;
261
- }
262
- if (normalized.includes("/.pi/") && file.startsWith("automode")) return true;
263
- if (
264
- normalized.includes("/pi-automode/") ||
265
- (isInside(policyPath, policyCwd) && file.includes("auto-mode"))
266
- ) {
267
- return true;
268
- }
269
- return false;
270
- }
271
-
272
- export function shellPathTokenToPath(
273
- token: string,
274
- cwd: string,
275
- shellText = token,
276
- ): string | undefined {
277
- let value = token.trim();
278
- if (!value || value === "-" || value.startsWith("&")) return undefined;
279
- value = value
280
- .replace(/^\$HOME(?=\/|$)/, HOME)
281
- .replace(/^\$\{HOME\}(?=\/|$)/, HOME);
282
- if (shellText === "~") value = HOME;
283
- else if (shellText.startsWith("~/")) value = resolve(HOME, value.slice(2));
284
- return isAbsolute(value) ? resolve(value) : resolve(cwd, value);
285
- }
286
-
287
- export function isProfileOrAuthorizedKeysPath(
288
- path: string,
289
- ): string | undefined {
290
- if (PROFILE_FILES.has(path)) {
291
- return "shell profile modification is hard-denied";
292
- }
293
- if (path === resolve(HOME, ".ssh/authorized_keys")) {
294
- return "SSH authorized_keys modification is hard-denied";
295
- }
296
- return undefined;
297
- }