@zeldrisho/pi-gate 0.7.2 → 0.7.4
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/CHANGELOG.md +16 -1
- package/README.md +1 -3
- package/package.json +3 -6
- package/src/approval-prompt.ts +46 -7
- package/src/index.ts +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.4] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Shorten approval timeout and cancellation notices.
|
|
15
|
+
- Remove the package's Node.js engine restriction; the current Node.js LTS release is the tested runtime.
|
|
16
|
+
|
|
17
|
+
## [0.7.3] - 2026-10-28
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Show the approval timeout and configured confirmation keys, and allow horizontal scrolling of long command lines.
|
|
22
|
+
|
|
10
23
|
## [0.7.2] - 2026-10-25
|
|
11
24
|
|
|
12
25
|
### Changed
|
|
@@ -112,7 +125,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
112
125
|
- support `prompt`, `block`, and `allow` actions, with longest-pattern-wins resolution
|
|
113
126
|
- block rather than auto-approve in non-UI modes
|
|
114
127
|
|
|
115
|
-
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.
|
|
128
|
+
[Unreleased]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.4...HEAD
|
|
129
|
+
[0.7.4]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.3...pi-gate-v0.7.4
|
|
130
|
+
[0.7.3]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.2...pi-gate-v0.7.3
|
|
116
131
|
[0.7.2]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.1...pi-gate-v0.7.2
|
|
117
132
|
[0.7.1]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.7.0...pi-gate-v0.7.1
|
|
118
133
|
[0.7.0]: https://github.com/zeldrisho/pi-packages/compare/pi-gate-v0.6.1...pi-gate-v0.7.0
|
package/README.md
CHANGED
|
@@ -46,9 +46,7 @@ pi-gate is a guard rail, not a sandbox. Complex shell syntax, pipelines, redirec
|
|
|
46
46
|
|
|
47
47
|
The built-in `bash` tool is gated, and `write`/`edit` calls targeting the gate config require approval. Prompt dialogs offer `Allow` and `Deny`; approvals are never remembered. To stop repeated prompts, add a specific `allow` rule to `gate.json`.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
The matched command text is highlighted in the TUI approval dialog. Long commands are truncated in approval dialogs, but an approved command executes in full. Denials, policy blocks, dismissals, and timeouts stop the turn; the agent is told to ask before retrying a denial or proceeding past a block, while a timeout asks whether to retry approval. Other allowed calls in a parallel batch may still run. In non-interactive modes (`-p`, JSON), commands requiring approval are denied.
|
|
49
|
+
The approval dialog highlights the matched command text. Long commands are truncated in the dialog but execute in full if approved. Denials, blocks, dismissals, and timeouts stop the turn; other allowed calls in a parallel batch may still run. In non-interactive modes (`-p`, JSON), commands requiring approval are denied.
|
|
52
50
|
|
|
53
51
|
In Herdr-managed panes, install Herdr's Pi integration with `herdr integration install pi` to report pending approvals as `blocked`. Herdr is optional.
|
|
54
52
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeldrisho/pi-gate",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Pi extension that gates bash commands and protects its configuration from tool edits",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"command-blocking",
|
|
@@ -36,16 +36,13 @@
|
|
|
36
36
|
"@earendil-works/pi-tui": "^1.4.0",
|
|
37
37
|
"typebox": "^1.4.2",
|
|
38
38
|
"typescript": "^7.0.2",
|
|
39
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@1.
|
|
40
|
-
"vite-plus": "1.
|
|
39
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
|
|
40
|
+
"vite-plus": "1.4.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@earendil-works/pi-coding-agent": "*",
|
|
44
44
|
"@earendil-works/pi-tui": "*"
|
|
45
45
|
},
|
|
46
|
-
"engines": {
|
|
47
|
-
"node": ">=24"
|
|
48
|
-
},
|
|
49
46
|
"pi": {
|
|
50
47
|
"extensions": [
|
|
51
48
|
"./src/index.ts"
|
package/src/approval-prompt.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { keyHint, rawKeyHint } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Key,
|
|
5
|
+
matchesKey,
|
|
6
|
+
sliceByColumn,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
visibleWidth,
|
|
9
|
+
wrapTextWithAnsi,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
3
11
|
|
|
4
12
|
const ACTIONS = ["Allow", "Deny"] as const;
|
|
5
13
|
|
|
@@ -31,8 +39,12 @@ export function createApprovalPrompt(
|
|
|
31
39
|
done: (result: Action | undefined) => void,
|
|
32
40
|
command: string,
|
|
33
41
|
rule: string,
|
|
42
|
+
deadline?: number,
|
|
34
43
|
): Component {
|
|
35
44
|
let selectedIndex = 0;
|
|
45
|
+
let horizontalOffset = 0;
|
|
46
|
+
let maxHorizontalOffset = 0;
|
|
47
|
+
let pageWidth = 1;
|
|
36
48
|
|
|
37
49
|
const invalidate = () => {};
|
|
38
50
|
|
|
@@ -55,11 +67,26 @@ export function createApprovalPrompt(
|
|
|
55
67
|
const component: Component = {
|
|
56
68
|
render(width: number): string[] {
|
|
57
69
|
const available = innerWidth(width);
|
|
70
|
+
|
|
71
|
+
const longestLine = Math.max(
|
|
72
|
+
0,
|
|
73
|
+
...command.split("\n").map((line) => visibleWidth(line.replace(/[»«]/gu, ""))),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
pageWidth = longestLine > available ? Math.max(1, available - 2) : available;
|
|
77
|
+
maxHorizontalOffset = longestLine > available ? longestLine - pageWidth : 0;
|
|
78
|
+
horizontalOffset = Math.min(horizontalOffset, maxHorizontalOffset);
|
|
58
79
|
const border = theme.style("─".repeat(available + 4), { fg: "border" });
|
|
59
80
|
|
|
60
81
|
const lines = [
|
|
61
82
|
border,
|
|
62
|
-
panelLine(
|
|
83
|
+
panelLine(
|
|
84
|
+
theme.bold(
|
|
85
|
+
`${deadline === undefined ? "" : `(${Math.max(0, Math.ceil((deadline - Date.now()) / 1_000))}s) `}pi-gate: allow this command?`,
|
|
86
|
+
),
|
|
87
|
+
width,
|
|
88
|
+
"accent",
|
|
89
|
+
),
|
|
63
90
|
panelLine("Review the requested command or file change:", width, "muted"),
|
|
64
91
|
panelLine("", width),
|
|
65
92
|
...command.split("\n").map((line) => {
|
|
@@ -86,10 +113,15 @@ export function createApprovalPrompt(
|
|
|
86
113
|
.join("");
|
|
87
114
|
|
|
88
115
|
const visible = line.replace(/[»«]/gu, "");
|
|
89
|
-
const content =
|
|
90
|
-
const
|
|
116
|
+
const content = sliceByColumn(rendered, horizontalOffset, pageWidth, true);
|
|
117
|
+
const prefix = horizontalOffset > 0 && visibleWidth(visible) > 0 ? "…" : "";
|
|
118
|
+
const suffix = visibleWidth(visible) > horizontalOffset + pageWidth ? "…" : "";
|
|
119
|
+
|
|
120
|
+
const padding = " ".repeat(
|
|
121
|
+
Math.max(0, contentWidth - visibleWidth(prefix + content + suffix)),
|
|
122
|
+
);
|
|
91
123
|
|
|
92
|
-
return ` ${
|
|
124
|
+
return ` ${theme.style(prefix, { fg: "toolOutput" })}${content}${theme.style(suffix + padding, { fg: "toolOutput" })} `;
|
|
93
125
|
}),
|
|
94
126
|
panelLine("", width),
|
|
95
127
|
panelLine(`Matched rule: ${rule}`, width, "muted"),
|
|
@@ -105,14 +137,21 @@ export function createApprovalPrompt(
|
|
|
105
137
|
});
|
|
106
138
|
}),
|
|
107
139
|
panelLine("", width),
|
|
108
|
-
|
|
140
|
+
...wrapTextWithAnsi(
|
|
141
|
+
`${rawKeyHint("↑↓", "navigate")} ${maxHorizontalOffset > 0 ? `${rawKeyHint("←→", "scroll command")} ` : ""}${keyHint("tui.select.confirm", "select")} ${keyHint("tui.select.cancel", "cancel")}`,
|
|
142
|
+
available,
|
|
143
|
+
).map((line) => ` ${line} `),
|
|
109
144
|
border,
|
|
110
145
|
];
|
|
111
146
|
|
|
112
147
|
return lines.map((line) => truncateToWidth(line, width));
|
|
113
148
|
},
|
|
114
149
|
handleInput(data: string): void {
|
|
115
|
-
if (
|
|
150
|
+
if (matchesKey(data, Key.left)) {
|
|
151
|
+
horizontalOffset = Math.max(0, horizontalOffset - pageWidth);
|
|
152
|
+
} else if (matchesKey(data, Key.right)) {
|
|
153
|
+
horizontalOffset = Math.min(maxHorizontalOffset, horizontalOffset + pageWidth);
|
|
154
|
+
} else if (keybindings.matches(data, "tui.select.up") || data === "k") {
|
|
116
155
|
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
117
156
|
} else if (keybindings.matches(data, "tui.select.down") || data === "j") {
|
|
118
157
|
selectedIndex = Math.min(ACTIONS.length - 1, selectedIndex + 1);
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ async function selectApproval(
|
|
|
66
66
|
let uiDone: ((value: ApprovalAction | undefined) => void) | undefined;
|
|
67
67
|
let settled = false;
|
|
68
68
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
69
|
+
let countdown: ReturnType<typeof setInterval> | undefined;
|
|
69
70
|
let removeAbortListener: (() => void) | undefined;
|
|
70
71
|
|
|
71
72
|
return new Promise<ApprovalAction | undefined>((resolve, reject) => {
|
|
@@ -74,6 +75,8 @@ async function selectApproval(
|
|
|
74
75
|
settled = true;
|
|
75
76
|
|
|
76
77
|
if (timer !== undefined) clearTimeout(timer);
|
|
78
|
+
|
|
79
|
+
if (countdown !== undefined) clearInterval(countdown);
|
|
77
80
|
removeAbortListener?.();
|
|
78
81
|
resolve(value);
|
|
79
82
|
uiDone?.(value);
|
|
@@ -82,13 +85,15 @@ async function selectApproval(
|
|
|
82
85
|
const onAbort = () => finish(undefined);
|
|
83
86
|
|
|
84
87
|
try {
|
|
88
|
+
const deadline = Date.now() + timeoutMs;
|
|
85
89
|
timer = setTimeout(() => finish(undefined), timeoutMs);
|
|
86
90
|
|
|
87
91
|
const uiPromise = ctx.ui.custom<ApprovalAction | undefined>(
|
|
88
92
|
(tui, theme, keybindings, done) => {
|
|
89
93
|
uiDone = done;
|
|
94
|
+
countdown = setInterval(() => tui.requestRender(), 1_000);
|
|
90
95
|
|
|
91
|
-
return createApprovalPrompt(tui, theme, keybindings, done, command, rule);
|
|
96
|
+
return createApprovalPrompt(tui, theme, keybindings, done, command, rule, deadline);
|
|
92
97
|
},
|
|
93
98
|
);
|
|
94
99
|
|
|
@@ -97,6 +102,8 @@ async function selectApproval(
|
|
|
97
102
|
.catch(reject)
|
|
98
103
|
.finally(() => {
|
|
99
104
|
if (timer !== undefined) clearTimeout(timer);
|
|
105
|
+
|
|
106
|
+
if (countdown !== undefined) clearInterval(countdown);
|
|
100
107
|
removeAbortListener?.();
|
|
101
108
|
});
|
|
102
109
|
|
|
@@ -110,6 +117,8 @@ async function selectApproval(
|
|
|
110
117
|
settled = true;
|
|
111
118
|
|
|
112
119
|
if (timer !== undefined) clearTimeout(timer);
|
|
120
|
+
|
|
121
|
+
if (countdown !== undefined) clearInterval(countdown);
|
|
113
122
|
removeAbortListener?.();
|
|
114
123
|
reject(error);
|
|
115
124
|
}
|
|
@@ -129,7 +138,7 @@ function reasonRuleName(pattern: string): string {
|
|
|
129
138
|
|
|
130
139
|
/** Formats an approval timeout with the matched rule, elapsed limit in seconds, and retry guidance. */
|
|
131
140
|
function timeoutReason(pattern: string, timeoutMs: number): string {
|
|
132
|
-
return `pi-gate:
|
|
141
|
+
return `pi-gate: approval timed out after ${timeoutMs / 1_000}s (rule "${reasonRuleName(pattern)}"); command not run. Ask the user before retrying.`;
|
|
133
142
|
}
|
|
134
143
|
|
|
135
144
|
function reportHerdrBlocked(pi: ExtensionAPI, ctx: { mode?: string }, active: boolean): void {
|
|
@@ -333,7 +342,7 @@ export default function piGate(pi: ExtensionAPI): void {
|
|
|
333
342
|
return {
|
|
334
343
|
block: true,
|
|
335
344
|
reason:
|
|
336
|
-
"pi-gate: approval cancelled or policy changed; command not run. Ask the user
|
|
345
|
+
"pi-gate: approval cancelled or policy changed; command not run. Ask the user before retrying.",
|
|
337
346
|
terminate: true,
|
|
338
347
|
};
|
|
339
348
|
}
|