@codersbrew/pi-tools 0.1.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 CodersBrew
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,118 @@
1
+ # @codersbrew/pi-tools
2
+
3
+ A publishable [pi](https://github.com/badlogic/pi-mono) package that bundles CodersBrew's custom pi extensions.
4
+
5
+ ## Included extensions
6
+
7
+ ### `security`
8
+ Protects common dangerous tool operations by:
9
+ - warning or blocking risky `bash` commands such as `rm -rf`, `sudo`, and destructive disk operations
10
+ - blocking writes to sensitive paths like `.env`, `.git`, `node_modules`, SSH keys, and common secrets files
11
+ - prompting before lockfile edits such as `package-lock.json`, `yarn.lock`, and `pnpm-lock.yaml`
12
+
13
+ ### `session-breakdown`
14
+ Adds an interactive TUI for analyzing pi session history from `~/.pi/agent/sessions`, including:
15
+ - sessions, messages, tokens, and cost over the last 7 / 30 / 90 days
16
+ - model, cwd, day-of-week, and time-of-day breakdowns
17
+ - contribution-style heatmap visualizations
18
+
19
+ ## Install
20
+
21
+ Global install:
22
+
23
+ ```bash
24
+ pi install npm:@codersbrew/pi-tools
25
+ ```
26
+
27
+ Project-local install:
28
+
29
+ ```bash
30
+ pi install -l npm:@codersbrew/pi-tools
31
+ ```
32
+
33
+ You can also add it manually to pi settings:
34
+
35
+ ```json
36
+ {
37
+ "packages": ["npm:@codersbrew/pi-tools"]
38
+ }
39
+ ```
40
+
41
+ ## Package structure
42
+
43
+ This package uses pi's standard package manifest:
44
+ - `extensions/security.ts`
45
+ - `extensions/session-breakdown.ts`
46
+
47
+ The root `package.json` exposes them through:
48
+
49
+ ```json
50
+ {
51
+ "pi": {
52
+ "extensions": ["./extensions"]
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Local development
58
+
59
+ Install dependencies:
60
+
61
+ ```bash
62
+ npm install
63
+ ```
64
+
65
+ Run local verification:
66
+
67
+ ```bash
68
+ npm run check
69
+ ```
70
+
71
+ Preview the publish tarball:
72
+
73
+ ```bash
74
+ npm pack --dry-run
75
+ ```
76
+
77
+ ## Releasing
78
+
79
+ This repo includes `.github/workflows/release.yml`.
80
+
81
+ ### First release bootstrap
82
+
83
+ Because npm trusted publishing is configured per existing package, the **first publish** of a brand new package must use an `NPM_TOKEN` GitHub secret.
84
+
85
+ Bootstrap steps for `@codersbrew/pi-tools`:
86
+
87
+ 1. Create an npm access token with publish rights
88
+ 2. Add it to the GitHub repo as `NPM_TOKEN`
89
+ 3. Confirm `package.json` has the intended version, currently `0.1.0`
90
+ 4. Push a version tag such as `v0.1.0`
91
+ 5. GitHub Actions will verify the package, publish it, and create a GitHub release
92
+
93
+ ### Switch to trusted publishing after first release
94
+
95
+ After the package exists on npm, configure npm Trusted Publisher for this repository:
96
+
97
+ - **Provider:** GitHub Actions
98
+ - **Organization or user:** `codersbrew`
99
+ - **Repository:** `pi-tools`
100
+ - **Workflow filename:** `release.yml`
101
+ - **Environment name:** leave blank unless you later add a GitHub Environment
102
+
103
+ After trusted publishing is configured, remove the `NPM_TOKEN` secret if you want future releases to publish via GitHub OIDC instead of a token.
104
+
105
+ ### Ongoing release flow
106
+
107
+ 1. Update the package version in `package.json`
108
+ 2. Commit and push the change
109
+ 3. Create and push a matching version tag such as `v0.1.1`
110
+ 4. GitHub Actions will run verification, publish to npm, and create a GitHub release
111
+
112
+ The workflow uses Node 24 so the npm CLI is new enough for trusted publishing and provenance support.
113
+
114
+ If you use manual dispatch, ensure the version in `package.json` has not already been published.
115
+
116
+ ## License
117
+
118
+ MIT
@@ -0,0 +1,113 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import * as path from "node:path";
3
+
4
+ /**
5
+ * Comprehensive security hook:
6
+ * - Blocks dangerous bash commands (rm -rf, sudo, chmod 777, etc.)
7
+ * - Protects sensitive paths from writes (.env, node_modules, .git, keys)
8
+ */
9
+ export default function (pi: ExtensionAPI) {
10
+ const dangerousCommands = [
11
+ { pattern: /\brm\s+(-[^\s]*r|--recursive)/, desc: "recursive delete" }, // rm -rf, rm -r, rm --recursive
12
+ { pattern: /\bsudo\b/, desc: "sudo command" }, // sudo anything
13
+ { pattern: /\b(chmod|chown)\b.*777/, desc: "dangerous permissions" }, // chmod 777, chown 777
14
+ { pattern: /\bmkfs\b/, desc: "filesystem format" }, // mkfs.ext4, mkfs.xfs
15
+ { pattern: /\bdd\b.*\bof=\/dev\//, desc: "raw device write" }, // dd if=x of=/dev/sda
16
+ { pattern: />\s*\/dev\/sd[a-z]/, desc: "raw device overwrite" }, // echo x > /dev/sda
17
+ { pattern: /\bkill\s+-9\s+-1\b/, desc: "kill all processes" }, // kill -9 -1
18
+ { pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/, desc: "fork bomb" }, // :(){:|:&};:
19
+ ];
20
+
21
+ const protectedPaths = [
22
+ { pattern: /\.env($|\.(?!example))/, desc: "environment file" }, // .env, .env.local (but not .env.example)
23
+ { pattern: /\.dev\.vars($|\.[^/]+$)/, desc: "dev vars file" }, // .dev.vars
24
+ { pattern: /node_modules\//, desc: "node_modules" }, // node_modules/
25
+ { pattern: /^\.git\/|\/\.git\//, desc: "git directory" }, // .git/
26
+ { pattern: /\.pem$|\.key$/, desc: "private key file" }, // *.pem, *.key
27
+ { pattern: /id_rsa|id_ed25519|id_ecdsa/, desc: "SSH key" }, // id_rsa, id_ed25519
28
+ { pattern: /\.ssh\//, desc: ".ssh directory" }, // .ssh/
29
+ { pattern: /secrets?\.(json|ya?ml|toml)$/i, desc: "secrets file" }, // secrets.json, secret.yaml
30
+ { pattern: /credentials/i, desc: "credentials file" }, // credentials, CREDENTIALS
31
+ ];
32
+
33
+ const softProtectedPaths = [
34
+ { pattern: /package-lock\.json$/, desc: "package-lock.json" },
35
+ { pattern: /yarn\.lock$/, desc: "yarn.lock" },
36
+ { pattern: /pnpm-lock\.yaml$/, desc: "pnpm-lock.yaml" },
37
+ ];
38
+
39
+ const dangerousBashWrites = [
40
+ />\s*\.env(?!\.example)(\b|$)/, // echo x > .env, .env.local (but not .env.example)
41
+ />\s*\.dev\.vars/, // echo x > .dev.vars
42
+ />\s*.*\.pem/, // echo x > key.pem
43
+ />\s*.*\.key/, // echo x > secret.key
44
+ /tee\s+.*\.env(?!\.example)(\b|$)/, // cat x | tee .env, .env.local (but not .env.example)
45
+ /tee\s+.*\.dev\.vars/, // cat x | tee .dev.vars
46
+ /cp\s+.*\s+\.env(?!\.example)(\b|$)/, // cp x .env, .env.local (but not .env.example)
47
+ /mv\s+.*\s+\.env(?!\.example)(\b|$)/, // mv x .env, .env.local (but not .env.example)
48
+ ];
49
+
50
+ pi.on("tool_call", async (event, ctx) => {
51
+ if (event.toolName === "bash") {
52
+ const command = event.input.command as string;
53
+
54
+ for (const { pattern, desc } of dangerousCommands) {
55
+ if (pattern.test(command)) {
56
+ if (!ctx.hasUI) {
57
+ return { block: true, reason: `Blocked ${desc} (no UI to confirm)` };
58
+ }
59
+
60
+ const ok = await ctx.ui.confirm(`⚠️ Dangerous command: ${desc}`, command);
61
+
62
+ if (!ok) {
63
+ return { block: true, reason: `Blocked ${desc} by user` };
64
+ }
65
+ break;
66
+ }
67
+ }
68
+
69
+ for (const pattern of dangerousBashWrites) {
70
+ if (pattern.test(command)) {
71
+ ctx.ui.notify(`🛡️ Blocked bash write to protected path`, "warning");
72
+ return { block: true, reason: "Bash command writes to protected path" };
73
+ }
74
+ }
75
+
76
+ return undefined;
77
+ }
78
+
79
+ if (event.toolName === "write" || event.toolName === "edit") {
80
+ const filePath = event.input.path as string;
81
+ const normalizedPath = path.normalize(filePath);
82
+
83
+ for (const { pattern, desc } of protectedPaths) {
84
+ if (pattern.test(normalizedPath)) {
85
+ ctx.ui.notify(`🛡️ Blocked write to ${desc}: ${filePath}`, "warning");
86
+ return { block: true, reason: `Protected path: ${desc}` };
87
+ }
88
+ }
89
+
90
+ for (const { pattern, desc } of softProtectedPaths) {
91
+ if (pattern.test(normalizedPath)) {
92
+ if (!ctx.hasUI) {
93
+ return { block: true, reason: `Protected path (no UI): ${desc}` };
94
+ }
95
+
96
+ const ok = await ctx.ui.confirm(
97
+ `⚠️ Modifying ${desc}`,
98
+ `Are you sure you want to modify ${filePath}?`,
99
+ );
100
+
101
+ if (!ok) {
102
+ return { block: true, reason: `User blocked write to ${desc}` };
103
+ }
104
+ break;
105
+ }
106
+ }
107
+
108
+ return undefined;
109
+ }
110
+
111
+ return undefined;
112
+ });
113
+ }