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 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
+ ```
@@ -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
+ }