@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.
- package/README.md +3 -15
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
- package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
- package/node_modules/@herbertgao/sol-pi/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
- package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
- package/node_modules/@tifan/pi-copy-response/README.md +2 -2
- package/node_modules/@tifan/pi-copy-response/package.json +3 -2
- package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
- package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
- package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
- package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
- package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
- package/node_modules/@tifan/pi-recap/package.json +3 -2
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
- package/node_modules/pi-mcp-adapter/README.md +125 -19
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
- package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
- package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
- package/node_modules/pi-mcp-adapter/commands.ts +13 -15
- package/node_modules/pi-mcp-adapter/config.ts +198 -30
- package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
- package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
- package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
- package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
- package/node_modules/pi-mcp-adapter/index.ts +827 -164
- package/node_modules/pi-mcp-adapter/init.ts +48 -10
- package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
- package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
- package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
- package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
- package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
- package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
- package/node_modules/pi-mcp-adapter/package.json +13 -3
- package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
- package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
- package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
- package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
- package/node_modules/pi-mcp-adapter/state.ts +10 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
- package/node_modules/pi-mcp-adapter/types.ts +57 -4
- package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
- package/node_modules/pi-mcp-adapter/utils.ts +64 -6
- package/package.json +13 -21
- package/examples/pi-footer.json +0 -275
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
- package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
- package/node_modules/@czottmann/pi-automode/README.md +0 -215
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
- package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
- package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
- package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
- package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
- package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
- package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
- package/node_modules/@czottmann/pi-automode/package.json +0 -61
- 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
|
-
}
|