@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 +21 -0
- package/README.md +118 -0
- package/extensions/security.ts +113 -0
- package/extensions/session-breakdown.ts +1629 -0
- package/package.json +54 -0
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
|
+
}
|