pi-auto-reviewer 1.0.0
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/LICENSE +21 -0
- package/README.md +87 -0
- package/auto-reviewer.ts +325 -0
- package/package.json +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vinzenz Richard Ulrich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# pi-auto-reviewer
|
|
2
|
+
|
|
3
|
+
Automatically review bash commands that your pi agent wants to execute - akin to Codex "Auto-review" and Claude Code "auto mode".
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
Every bash command the agent wants to run goes through three tiers:
|
|
8
|
+
|
|
9
|
+
| Tier | Action | Examples |
|
|
10
|
+
|------|--------|----------|
|
|
11
|
+
| **1. Auto-permitted** | Runs immediately | `ls`, `cd`, `grep`, `git status`, `npm list`, `echo` |
|
|
12
|
+
| **2. Auto-blocked** | Refused immediately | `rm -rf`, `sudo`, `chmod 777`, `git push --force`, `shutdown` |
|
|
13
|
+
| **3. Needs review** | Sent to a reviewer LLM | `git commit`, `npm install`, `curl`, `mv`, `sed -i`, `cp` |
|
|
14
|
+
|
|
15
|
+
When a command falls into **Tier 3**, a subagent LLM reviews the command with project context and decides ALLOW or BLOCK.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
### All projects (global)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cp auto-reviewer.ts ~/.pi/agent/extensions/
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Single project
|
|
26
|
+
|
|
27
|
+
Copy the extension into your project:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cp auto-reviewer.ts .pi/extensions/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pi auto-discovers extensions in `.pi/extensions/` when the project is trusted.
|
|
34
|
+
|
|
35
|
+
### Single session
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pi -e ./auto-reviewer.ts
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
Once installed, it works automatically - no configuration needed. Every bash command the agent tries to run will be reviewed.
|
|
44
|
+
|
|
45
|
+
### What to expect
|
|
46
|
+
|
|
47
|
+
- **Safe commands** (Tier 1) run without any visible delay.
|
|
48
|
+
- **Dangerous commands** (Tier 2) are blocked with a notification explaining why.
|
|
49
|
+
- **Everything else** (Tier 3) pauses briefly while the reviewer LLM decides. You'll see a status message: `Reviewing: <command>...`
|
|
50
|
+
|
|
51
|
+
- If **allowed**: the command runs and you see `Auto-reviewer: ✓ <reason>`
|
|
52
|
+
- If **blocked**: the command is refused and you see `Auto-reviewer: ✗ <reason>`
|
|
53
|
+
- If the reviewer **fails** (timeout, error): you're prompted interactively to allow or deny manually.
|
|
54
|
+
|
|
55
|
+
### Non-interactive mode
|
|
56
|
+
|
|
57
|
+
In print mode (`pi -p`) or JSON mode, Tier 3 commands are blocked by default since there's no UI to fall back on.
|
|
58
|
+
|
|
59
|
+
## Customizing review rules
|
|
60
|
+
|
|
61
|
+
Edit `AUTO_PERMITTED` and `AUTO_BLOCKED` arrays in `auto-reviewer.ts` to add or remove patterns. Edit `buildReviewPrompt()` to change how the reviewer LLM decides.
|
|
62
|
+
|
|
63
|
+
## Publishing to the pi package gallery
|
|
64
|
+
|
|
65
|
+
1. Publish to npm:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm publish
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
2. The [pi.dev/packages](https://pi.dev/packages) gallery automatically discovers packages tagged with `"pi-package"`. Once published, users can install it with:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pi install npm:pi-auto-reviewer
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or try it in a single session:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pi -e npm:pi-auto-reviewer
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
To publish via git instead of npm, push to a public repo and users install with:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pi install git:github.com/your-username/pi-auto-reviewer
|
|
87
|
+
```
|
package/auto-reviewer.ts
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Reviewer Extension
|
|
3
|
+
*
|
|
4
|
+
* Auto-reviews bash commands before execution, similar to Codex's auto-reviewer.
|
|
5
|
+
*
|
|
6
|
+
* Three tiers:
|
|
7
|
+
* 1. Auto-permitted: safe commands (ls, cd, grep, git status, etc.)
|
|
8
|
+
* 2. Auto-blocked: obviously dangerous (rm -rf, sudo, chmod 777)
|
|
9
|
+
* 3. Needs review: everything else → call a subagent LLM to decide
|
|
10
|
+
*
|
|
11
|
+
* The reviewer subagent gets: the command, current directory, and project context.
|
|
12
|
+
* It returns a decision (allow/block) with a reason.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
|
|
21
|
+
// ── Tier 1: Auto-permitted command patterns ──
|
|
22
|
+
//
|
|
23
|
+
// These are regexps tested against the full command string.
|
|
24
|
+
// The model will never see these — they bypass review entirely.
|
|
25
|
+
const AUTO_PERMITTED = [
|
|
26
|
+
// Read-only directory listing
|
|
27
|
+
/^(ls|dir|tree)\b/,
|
|
28
|
+
// Directory navigation
|
|
29
|
+
/^cd\b/,
|
|
30
|
+
// Read-only file ops
|
|
31
|
+
/^(cat|head|tail|less|more)\b/,
|
|
32
|
+
/^(file|stat|wc|du|df)\b/,
|
|
33
|
+
// grep / rg / ag — read-only search
|
|
34
|
+
/^(grep|rg|ag|ack)\b/,
|
|
35
|
+
// find / locate — read-only
|
|
36
|
+
/^(find|locate|which|whereis|type)\b/,
|
|
37
|
+
// Git read-only operations
|
|
38
|
+
/^git\s+(status|log|diff|show|branch|tag|stash\s+list|remote|ls-remote|rev-parse|rev-list|describe|whatchanged|shortlog|blame|grep|config\s+--get|config\s+--list|config\s+-l)\b/,
|
|
39
|
+
/^git\s+log\b/,
|
|
40
|
+
// Docker/container read-only
|
|
41
|
+
/^(docker|podman)\s+(ps|images|inspect|logs|stats|info|version|history|top|diff)\b/,
|
|
42
|
+
// Package manager info/list
|
|
43
|
+
/^(npm|yarn|pnpm)\s+(list|info|view|outdated|audit|why|config\s+list)\b/,
|
|
44
|
+
/^(pip|pip3)\s+(list|show|freeze|search)\b/,
|
|
45
|
+
/^(cargo|go)\s+(search|doc)\b/,
|
|
46
|
+
// System info
|
|
47
|
+
/^(echo|printenv|env|whoami|hostname|uname|uptime|id|groups|pwd|date)\b/,
|
|
48
|
+
// Python/node one-off checks (no args = safe)
|
|
49
|
+
/^(python3?|node|uv|tsx|npx)\s+(--version|-v|--help|-h)$/,
|
|
50
|
+
// Help flags
|
|
51
|
+
/^.*\s+(--help|-h)\s*$/,
|
|
52
|
+
// Simple echo (for env var checks, etc.)
|
|
53
|
+
/^echo\s/,
|
|
54
|
+
// Print working directory
|
|
55
|
+
/^pwd\b/,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// ── Tier 2: Auto-blocked patterns (never run, never ask) ──
|
|
59
|
+
const AUTO_BLOCKED = [
|
|
60
|
+
// Destructive file ops
|
|
61
|
+
/\brm\s+(-rf?|--recursive)\b/,
|
|
62
|
+
/\brm\s+(-rf?|--recursive)\s+\/\b/,
|
|
63
|
+
// Privilege escalation
|
|
64
|
+
/\bsudo\b/,
|
|
65
|
+
// Permission changes that are too open
|
|
66
|
+
/\bchmod\s+.*777/,
|
|
67
|
+
// Fork bombs and resource exhaustion
|
|
68
|
+
/:\(\)\s*\{/, // fork bomb pattern
|
|
69
|
+
// Disk destructive
|
|
70
|
+
/\bdd\s+if=/,
|
|
71
|
+
/\bmkfs\./,
|
|
72
|
+
// System shutdown
|
|
73
|
+
/\b(shutdown|reboot|halt|poweroff)\b/,
|
|
74
|
+
// Git destructive without review
|
|
75
|
+
/\bgit\s+(push\s+--force|reset\s+--hard|clean\s+-[fd]+)\b/,
|
|
76
|
+
// Direct /dev writes
|
|
77
|
+
/>\s*\/dev\//,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// ── Review prompt template ──
|
|
81
|
+
function buildReviewPrompt(command: string, cwd: string): string {
|
|
82
|
+
const projectName = path.basename(cwd);
|
|
83
|
+
return `You are a security reviewer for a coding agent. You must decide whether to ALLOW or BLOCK the following bash command.
|
|
84
|
+
|
|
85
|
+
=== COMMAND ===
|
|
86
|
+
${command}
|
|
87
|
+
|
|
88
|
+
=== CURRENT DIRECTORY ===
|
|
89
|
+
${cwd}
|
|
90
|
+
|
|
91
|
+
=== PROJECT ===
|
|
92
|
+
${projectName}
|
|
93
|
+
|
|
94
|
+
=== REVIEW RULES ===
|
|
95
|
+
1. Commands that ONLY read files, list directories, show info, or display state → ALLOW
|
|
96
|
+
2. Commands that modify files or system state → ALLOW if constructive (install deps, build, lint, format, test)
|
|
97
|
+
3. Commands that delete files, force-push, reset, or alter system config → BLOCK unless clearly intentional and scoped
|
|
98
|
+
4. Commands with environment variables like $SECRET or $TOKEN → BLOCK to prevent leaks
|
|
99
|
+
5. Commands that install from unverified sources (curl pipe bash, wget pipe sh) → BLOCK
|
|
100
|
+
6. Package manager installs (npm install, pip install, cargo add) → ALLOW (standard dev workflow)
|
|
101
|
+
7. Network operations like curl/wget to download files → ALLOW if to a project directory, BLOCK if suspicious
|
|
102
|
+
8. Any command that would affect files outside the project directory → BLOCK unless clearly a dev tool
|
|
103
|
+
|
|
104
|
+
=== RESPONSE FORMAT ===
|
|
105
|
+
Reply with ONLY one line:
|
|
106
|
+
- "ALLOW: <brief reason>" — to permit the command
|
|
107
|
+
- "BLOCK: <brief reason>" — to prevent the command
|
|
108
|
+
|
|
109
|
+
Do not include any other text, markdown, or explanation.`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Spawn a pi subprocess to review the command ──
|
|
113
|
+
async function reviewWithLLM(
|
|
114
|
+
command: string,
|
|
115
|
+
cwd: string,
|
|
116
|
+
signal: AbortSignal | undefined,
|
|
117
|
+
): Promise<{ allowed: boolean; reason: string }> {
|
|
118
|
+
const prompt = buildReviewPrompt(command, cwd);
|
|
119
|
+
|
|
120
|
+
// Write prompt to temp file
|
|
121
|
+
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-reviewer-"));
|
|
122
|
+
const promptPath = path.join(tmpDir, "review-prompt.md");
|
|
123
|
+
await fs.promises.writeFile(promptPath, prompt, { encoding: "utf8", mode: 0o600 });
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Resolve pi invocation
|
|
127
|
+
let piCmd: string;
|
|
128
|
+
let piArgs: string[];
|
|
129
|
+
const execPath = process.execPath;
|
|
130
|
+
const currentScript = process.argv[1];
|
|
131
|
+
|
|
132
|
+
if (currentScript && fs.existsSync(currentScript)) {
|
|
133
|
+
piCmd = execPath;
|
|
134
|
+
piArgs = [currentScript];
|
|
135
|
+
} else {
|
|
136
|
+
piCmd = "pi";
|
|
137
|
+
piArgs = [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
piArgs.push(
|
|
141
|
+
"--mode", "json", "-p",
|
|
142
|
+
"--no-session",
|
|
143
|
+
"--no-extensions",
|
|
144
|
+
"--no-context-files",
|
|
145
|
+
"--no-skills",
|
|
146
|
+
"--no-prompt-templates",
|
|
147
|
+
"--thinking", "minimal",
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Pass prompt as a positional argument (same approach as subagent example)
|
|
151
|
+
piArgs.push(prompt);
|
|
152
|
+
|
|
153
|
+
let capturedStderr = "";
|
|
154
|
+
|
|
155
|
+
const fullOutput = await new Promise<string>((resolve, reject) => {
|
|
156
|
+
const proc = spawn(piCmd, piArgs, {
|
|
157
|
+
cwd,
|
|
158
|
+
shell: false,
|
|
159
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let stdout = "";
|
|
163
|
+
|
|
164
|
+
proc.stdout.on("data", (data: Buffer) => { stdout += data.toString(); });
|
|
165
|
+
proc.stderr.on("data", (data: Buffer) => { capturedStderr += data.toString(); });
|
|
166
|
+
|
|
167
|
+
const timeout = setTimeout(() => {
|
|
168
|
+
proc.kill("SIGTERM");
|
|
169
|
+
reject(new Error("Review timed out after 15s"));
|
|
170
|
+
}, 15000);
|
|
171
|
+
|
|
172
|
+
proc.on("close", (code) => {
|
|
173
|
+
clearTimeout(timeout);
|
|
174
|
+
if (code === 0 || code === null) {
|
|
175
|
+
resolve(stdout.trim());
|
|
176
|
+
} else {
|
|
177
|
+
reject(new Error(`Reviewer exited with code ${code}: ${capturedStderr}`));
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
proc.on("error", (err) => {
|
|
182
|
+
clearTimeout(timeout);
|
|
183
|
+
reject(err);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (signal) {
|
|
187
|
+
const abortHandler = () => {
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
proc.kill("SIGTERM");
|
|
190
|
+
reject(new Error("Review aborted"));
|
|
191
|
+
};
|
|
192
|
+
if (signal.aborted) abortHandler();
|
|
193
|
+
else signal.addEventListener("abort", abortHandler, { once: true });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// DEBUG: dump full output to fixed temp file for inspection
|
|
198
|
+
const debugPath = path.join(os.tmpdir(), "pi-reviewer-debug.txt");
|
|
199
|
+
let debugContent = `=== DEBUG ${new Date().toISOString()} ===\n`;
|
|
200
|
+
debugContent += `STDOUT (${fullOutput.length} chars):\n${fullOutput}\n\n`;
|
|
201
|
+
debugContent += `STDERR (${capturedStderr.length} chars):\n${capturedStderr || "(empty)"}\n`;
|
|
202
|
+
await fs.promises.writeFile(debugPath, debugContent, { encoding: "utf8" });
|
|
203
|
+
|
|
204
|
+
// Parse: NDJSON output from `pi --mode json -p`.
|
|
205
|
+
// Each line is a JSON object. Extract text content from assistant
|
|
206
|
+
// messages and search for ALLOW/BLOCK decision within that text.
|
|
207
|
+
const lines = fullOutput.split("\n");
|
|
208
|
+
let decision: { allowed: boolean; reason: string } | null = null;
|
|
209
|
+
|
|
210
|
+
for (const line of lines) {
|
|
211
|
+
const trimmed = line.trim();
|
|
212
|
+
if (!trimmed) continue;
|
|
213
|
+
|
|
214
|
+
// Search for text content in JSON line (text_delta / text_end / message_end)
|
|
215
|
+
let searchText: string | null = null;
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(trimmed);
|
|
218
|
+
// message_end: extract text from assistant message content
|
|
219
|
+
if (parsed.type === "message_end" && parsed.message?.role === "assistant") {
|
|
220
|
+
for (const block of parsed.message.content || []) {
|
|
221
|
+
if (block.type === "text" && block.text) {
|
|
222
|
+
searchText = block.text;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// message_update with text_delta / text_end
|
|
228
|
+
if (!searchText && parsed.assistantMessageEvent) {
|
|
229
|
+
const evt = parsed.assistantMessageEvent;
|
|
230
|
+
if ((evt.type === "text_delta" || evt.type === "text_end") && evt.content) {
|
|
231
|
+
searchText = evt.content;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// Not valid JSON; treat trimmed line as plain text
|
|
236
|
+
searchText = trimmed;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (searchText) {
|
|
240
|
+
const allowMatch = searchText.match(/^ALLOW:\s*(.+)/i);
|
|
241
|
+
const blockMatch = searchText.match(/^BLOCK:\s*(.+)/i);
|
|
242
|
+
|
|
243
|
+
if (allowMatch) {
|
|
244
|
+
decision = { allowed: true, reason: allowMatch[1].trim() };
|
|
245
|
+
} else if (blockMatch) {
|
|
246
|
+
decision = { allowed: false, reason: blockMatch[1].trim() };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (decision) {
|
|
252
|
+
return decision;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Fallback: couldn't parse → block conservatively
|
|
256
|
+
return { allowed: false, reason: `Reviewer response unclear: "${fullOutput.slice(0, 200)}"` };
|
|
257
|
+
} finally {
|
|
258
|
+
// Cleanup temp files
|
|
259
|
+
try { fs.unlinkSync(promptPath); } catch { /* ignore */ }
|
|
260
|
+
try { fs.rmdirSync(tmpDir); } catch { /* ignore */ }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default function (pi: ExtensionAPI) {
|
|
265
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
266
|
+
if (event.toolName !== "bash") return undefined;
|
|
267
|
+
|
|
268
|
+
const command = (event.input.command as string).trim();
|
|
269
|
+
if (!command) return undefined;
|
|
270
|
+
|
|
271
|
+
// Tier 2: Auto-blocked
|
|
272
|
+
for (const pattern of AUTO_BLOCKED) {
|
|
273
|
+
if (pattern.test(command)) {
|
|
274
|
+
return { block: true, reason: `Auto-blocked: matches dangerous pattern "${pattern.source}"` };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Tier 1: Auto-permitted
|
|
279
|
+
for (const pattern of AUTO_PERMITTED) {
|
|
280
|
+
if (pattern.test(command)) {
|
|
281
|
+
return undefined; // allow through
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Tier 3: Needs review
|
|
286
|
+
if (!ctx.hasUI) {
|
|
287
|
+
// Non-interactive mode: block by default
|
|
288
|
+
return { block: true, reason: "Command requires review but no UI available" };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
ctx.ui.setStatus("auto-reviewer", `Reviewing: ${command.slice(0, 60)}...`);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const decision = await reviewWithLLM(command, ctx.cwd, ctx.signal);
|
|
295
|
+
|
|
296
|
+
ctx.ui.setStatus("auto-reviewer", undefined);
|
|
297
|
+
|
|
298
|
+
if (decision.allowed) {
|
|
299
|
+
ctx.ui.notify(`Auto-reviewer: ✓ ${decision.reason}`, "info");
|
|
300
|
+
return undefined; // allow through
|
|
301
|
+
} else {
|
|
302
|
+
ctx.ui.notify(`Auto-reviewer: ✗ ${decision.reason}`, "warning");
|
|
303
|
+
return { block: true, reason: `Auto-reviewer blocked: ${decision.reason}` };
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
ctx.ui.setStatus("auto-reviewer", undefined);
|
|
307
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
308
|
+
|
|
309
|
+
// On review failure, ask user
|
|
310
|
+
const choice = await ctx.ui.select(
|
|
311
|
+
`⚠️ Auto-review failed: ${msg}\n\nCommand: ${command}\n\nAllow?`,
|
|
312
|
+
["Yes", "No"],
|
|
313
|
+
);
|
|
314
|
+
if (choice !== "Yes") {
|
|
315
|
+
return { block: true, reason: "Auto-review failed and user declined" };
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Clean up status on session end
|
|
322
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
323
|
+
// No cleanup needed; status is session-scoped
|
|
324
|
+
});
|
|
325
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-auto-reviewer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Auto-review bash commands before your pi agent executes them — akin to Codex Auto-review and Claude Code auto mode.",
|
|
5
|
+
"keywords": ["pi-package"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"pi": {
|
|
8
|
+
"extensions": ["./auto-reviewer.ts"]
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
12
|
+
}
|
|
13
|
+
}
|