@diegopetrucci/pi-extensions 0.1.32 → 0.1.33

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/README.md CHANGED
@@ -5,6 +5,7 @@ A collection of [pi](https://github.com/earendil-works/pi-mono) agent extensions
5
5
  **License note:** This root collection package is mixed-licensed: most of it is MIT, while [`extensions/review`](./extensions/review) is Apache-2.0 and includes its own [LICENSE](./extensions/review/LICENSE).
6
6
 
7
7
  - [`agent-workflow-audit`](./extensions/agent-workflow-audit): Adds `/agent-workflow-audit`, which runs an isolated repo workflow audit subagent and returns only the final distilled report to the main session.
8
+ - [`brrr`](./extensions/brrr): Sends brrr push notifications when pi finishes an agent turn and is ready for input, with optional macOS idle gating.
8
9
  - [`confirm-destructive`](./extensions/confirm-destructive): Confirms before destructive session actions like clear, switch, and fork.
9
10
  - [`context-cap`](./extensions/context-cap): Caps effective model context windows at 200k tokens by default so pi avoids the `dumb zone`; toggle temporarily with `/context-cap`.
10
11
  - [`context-inspector`](./extensions/context-inspector): Adds `/context`, a local self-contained HTML dashboard that breaks down where the current session context is going, with category overview, top offenders, and drilldown search.
@@ -0,0 +1,84 @@
1
+ # brrr
2
+
3
+ A pi extension that sends [brrr](https://brrr.now) push notifications when pi finishes an agent turn and is ready for input.
4
+
5
+ ## Install
6
+
7
+ ### Standalone npm package
8
+
9
+ ```bash
10
+ pi install npm:@diegopetrucci/pi-brrr
11
+ ```
12
+
13
+ ### Collection package
14
+
15
+ ```bash
16
+ pi install npm:@diegopetrucci/pi-extensions
17
+ ```
18
+
19
+ ### GitHub package
20
+
21
+ ```bash
22
+ pi install git:github.com/diegopetrucci/pi-extensions
23
+ ```
24
+
25
+ Then reload pi:
26
+
27
+ ```text
28
+ /reload
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ Config files are merged, with project config overriding global config:
34
+
35
+ - `~/.pi/agent/extensions/brrr.json`
36
+ - `<project>/.pi/brrr.json`
37
+
38
+ The default config expects your webhook in `BRRR_WEBHOOK_URL`:
39
+
40
+ ```bash
41
+ export BRRR_WEBHOOK_URL='https://api.brrr.now/v1/br_...'
42
+ ```
43
+
44
+ A ready-to-copy sample file is included at [`brrr.example.json`](./brrr.example.json).
45
+
46
+ Example:
47
+
48
+ ```json
49
+ {
50
+ "enabled": true,
51
+ "onlyWhenInteractive": true,
52
+ "webhook": "$BRRR_WEBHOOK_URL",
53
+ "idleSeconds": 20,
54
+ "title": "Pi finished",
55
+ "message": "Pi finished working in '{project}'.",
56
+ "includeLastAssistantMessage": true,
57
+ "sound": "",
58
+ "openUrl": "",
59
+ "imageUrl": ""
60
+ }
61
+ ```
62
+
63
+ ## Commands
64
+
65
+ - `/brrr` shows whether notifications are enabled, whether the webhook resolves, and the idle threshold.
66
+
67
+ ## Config fields
68
+
69
+ - `enabled`: master on/off switch
70
+ - `onlyWhenInteractive`: skip notifications in print / non-UI mode
71
+ - `webhook`: brrr webhook URL or an environment reference like `$BRRR_WEBHOOK_URL`
72
+ - `idleSeconds`: only send when macOS has been idle for at least this many seconds; set to `null` to disable idle gating
73
+ - `title`: notification title; supports `{project}` and `{cwd}`
74
+ - `message`: fallback notification body; supports `{project}` and `{cwd}`
75
+ - `includeLastAssistantMessage`: use the final assistant message as the notification body when available
76
+ - `sound`: optional brrr sound value
77
+ - `openUrl`: optional URL to open from the notification
78
+ - `imageUrl`: optional image URL
79
+
80
+ ## Notes
81
+
82
+ - Hooks the `agent_end` event.
83
+ - The extension sends directly to the brrr webhook; it does not require the `brrr` CLI at runtime.
84
+ - By default, notifications are skipped unless the Mac has been idle for at least 20 seconds.
@@ -0,0 +1,12 @@
1
+ {
2
+ "enabled": true,
3
+ "onlyWhenInteractive": true,
4
+ "webhook": "$BRRR_WEBHOOK_URL",
5
+ "idleSeconds": 20,
6
+ "title": "Pi finished",
7
+ "message": "Pi finished working in '{project}'.",
8
+ "includeLastAssistantMessage": true,
9
+ "sound": "",
10
+ "openUrl": "",
11
+ "imageUrl": ""
12
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Pi brrr Extension
3
+ *
4
+ * Sends a brrr push notification when pi finishes an agent turn and is ready
5
+ * for more input.
6
+ *
7
+ * Config files (project overrides global):
8
+ * - ~/.pi/agent/extensions/brrr.json
9
+ * - <cwd>/.pi/brrr.json
10
+ */
11
+
12
+ import { execFile } from "node:child_process";
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { basename, join } from "node:path";
15
+ import { promisify } from "node:util";
16
+ import { getAgentDir, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
17
+
18
+ const execFileAsync = promisify(execFile);
19
+
20
+ interface BrrrConfig {
21
+ enabled: boolean;
22
+ onlyWhenInteractive: boolean;
23
+ webhook: string;
24
+ idleSeconds: number | null;
25
+ title: string;
26
+ message: string;
27
+ includeLastAssistantMessage: boolean;
28
+ sound: string;
29
+ openUrl: string;
30
+ imageUrl: string;
31
+ }
32
+
33
+ interface BrrrPayload {
34
+ message: string;
35
+ title?: string;
36
+ subtitle?: string;
37
+ expiration_date?: string;
38
+ sound?: string;
39
+ open_url?: string;
40
+ image_url?: string;
41
+ }
42
+
43
+ const DEFAULT_CONFIG: BrrrConfig = {
44
+ enabled: true,
45
+ onlyWhenInteractive: true,
46
+ webhook: "$BRRR_WEBHOOK_URL",
47
+ idleSeconds: 20,
48
+ title: "Pi finished",
49
+ message: "Pi finished working in '{project}'.",
50
+ includeLastAssistantMessage: true,
51
+ sound: "",
52
+ openUrl: "",
53
+ imageUrl: "",
54
+ };
55
+
56
+ function readConfigFile(path: string): Partial<BrrrConfig> {
57
+ if (!existsSync(path)) return {};
58
+
59
+ try {
60
+ return JSON.parse(readFileSync(path, "utf-8")) as Partial<BrrrConfig>;
61
+ } catch (error) {
62
+ console.error(`Warning: Could not parse ${path}: ${error}`);
63
+ return {};
64
+ }
65
+ }
66
+
67
+ function mergeConfig(base: BrrrConfig, overrides: Partial<BrrrConfig>): BrrrConfig {
68
+ return {
69
+ ...base,
70
+ ...overrides,
71
+ };
72
+ }
73
+
74
+ function loadConfig(cwd: string): BrrrConfig {
75
+ const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "brrr.json"));
76
+ const projectConfig = readConfigFile(join(cwd, ".pi", "brrr.json"));
77
+ return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
78
+ }
79
+
80
+ function resolveWebhook(raw: string): string | undefined {
81
+ const value = raw.trim();
82
+ if (!value) return undefined;
83
+
84
+ const envMatch = value.match(/^\$([A-Z0-9_]+)$/) ?? value.match(/^\$\{([A-Z0-9_]+)\}$/);
85
+ if (envMatch) return process.env[envMatch[1]]?.trim() || undefined;
86
+
87
+ return value;
88
+ }
89
+
90
+ function isBrrrWebhookUrl(value: string): boolean {
91
+ try {
92
+ const url = new URL(value);
93
+ return (
94
+ url.protocol === "https:" &&
95
+ (url.hostname === "api.brrr.now" || url.hostname === "dev.api.brrr.now") &&
96
+ /^\/v1\/br_[A-Za-z0-9_]+$/.test(url.pathname)
97
+ );
98
+ } catch {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ async function getMacOsIdleSeconds(): Promise<number | null> {
104
+ if (process.platform !== "darwin") return null;
105
+
106
+ try {
107
+ const { stdout } = await execFileAsync("ioreg", ["-c", "IOHIDSystem"], {
108
+ timeout: 1000,
109
+ maxBuffer: 1024 * 1024,
110
+ });
111
+ const match = stdout.match(/HIDIdleTime"\s*=\s*(\d+)/) ?? stdout.match(/HIDIdleTime\s+=\s+(\d+)/);
112
+ if (!match) return null;
113
+
114
+ const idleNanoseconds = Number(match[1]);
115
+ if (!Number.isFinite(idleNanoseconds)) return null;
116
+
117
+ return Math.floor(idleNanoseconds / 1_000_000_000);
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ async function shouldSkipForIdleThreshold(idleSeconds: number | null): Promise<boolean> {
124
+ if (idleSeconds === null) return false;
125
+ if (!Number.isFinite(idleSeconds) || idleSeconds < 0) return false;
126
+
127
+ const currentIdleSeconds = await getMacOsIdleSeconds();
128
+ if (currentIdleSeconds === null) return false;
129
+
130
+ return currentIdleSeconds < idleSeconds;
131
+ }
132
+
133
+ function formatTemplate(template: string, cwd: string): string {
134
+ const project = basename(cwd || process.cwd());
135
+ return template.replaceAll("{project}", project).replaceAll("{cwd}", cwd);
136
+ }
137
+
138
+ function extractTextContent(content: unknown): string {
139
+ if (typeof content === "string") return content;
140
+ if (!Array.isArray(content)) return "";
141
+
142
+ const parts: string[] = [];
143
+ for (const part of content) {
144
+ if (typeof part === "string") {
145
+ parts.push(part);
146
+ continue;
147
+ }
148
+ if (typeof part !== "object" || part === null) continue;
149
+
150
+ const maybeText = (part as { text?: unknown }).text;
151
+ if (typeof maybeText === "string") parts.push(maybeText);
152
+ }
153
+
154
+ return parts.join("\n");
155
+ }
156
+
157
+ function lastAssistantMessage(messages: readonly unknown[]): string | undefined {
158
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
159
+ const message = messages[index];
160
+ if (typeof message !== "object" || message === null) continue;
161
+ if ((message as { role?: unknown }).role !== "assistant") continue;
162
+
163
+ const text = extractTextContent((message as { content?: unknown }).content).trim();
164
+ if (text) return text;
165
+ }
166
+
167
+ return undefined;
168
+ }
169
+
170
+ function truncateMessage(value: string): string {
171
+ const trimmed = value.trim();
172
+ if (trimmed.length <= 800) return trimmed;
173
+ return `${trimmed.slice(0, 797).trimEnd()}...`;
174
+ }
175
+
176
+ async function sendBrrr(webhook: string, payload: BrrrPayload): Promise<{ ok: boolean; error?: string }> {
177
+ const controller = new AbortController();
178
+ const timeout = setTimeout(() => controller.abort(), 2_000);
179
+
180
+ try {
181
+ const body: Record<string, string> = { message: payload.message };
182
+ if (payload.title) body.title = payload.title;
183
+ if (payload.subtitle) body.subtitle = payload.subtitle;
184
+ if (payload.expiration_date) body.expiration_date = payload.expiration_date;
185
+ if (payload.sound) body.sound = payload.sound;
186
+ if (payload.open_url) body.open_url = payload.open_url;
187
+ if (payload.image_url) body.image_url = payload.image_url;
188
+
189
+ const response = await fetch(webhook, {
190
+ method: "POST",
191
+ headers: {
192
+ "content-type": "application/json",
193
+ accept: "application/json",
194
+ },
195
+ body: JSON.stringify(body),
196
+ signal: controller.signal,
197
+ });
198
+
199
+ if (response.status === 202) return { ok: true };
200
+ return { ok: false, error: `Unexpected response status ${response.status}.` };
201
+ } catch (error) {
202
+ return { ok: false, error: error instanceof Error ? error.message : "Unknown webhook failure." };
203
+ } finally {
204
+ clearTimeout(timeout);
205
+ }
206
+ }
207
+
208
+ function notify(ctx: ExtensionCommandContext, message: string, type: "info" | "warning" | "error" = "info"): void {
209
+ if (ctx.hasUI) ctx.ui.notify(message, type);
210
+ }
211
+
212
+ function describeConfig(config: BrrrConfig, webhook: string | undefined): string {
213
+ const webhookStatus = webhook ? (isBrrrWebhookUrl(webhook) ? "configured" : "invalid") : "missing";
214
+ const idle = config.idleSeconds === null ? "off" : `${config.idleSeconds}s`;
215
+ return `brrr is ${config.enabled ? "enabled" : "disabled"}; webhook ${webhookStatus}; idle threshold ${idle}.`;
216
+ }
217
+
218
+ export default function brrrExtension(pi: ExtensionAPI) {
219
+ pi.registerCommand("brrr", {
220
+ description: "Show brrr notification status",
221
+ handler: async (_args, ctx) => {
222
+ const config = loadConfig(ctx.cwd);
223
+ notify(ctx, describeConfig(config, resolveWebhook(config.webhook)));
224
+ },
225
+ });
226
+
227
+ pi.on("agent_end", async (event, ctx) => {
228
+ const config = loadConfig(ctx.cwd);
229
+ if (!config.enabled) return;
230
+ if (config.onlyWhenInteractive && !ctx.hasUI) return;
231
+
232
+ const webhook = resolveWebhook(config.webhook);
233
+ if (!webhook || !isBrrrWebhookUrl(webhook)) return;
234
+ if (await shouldSkipForIdleThreshold(config.idleSeconds)) return;
235
+
236
+ const assistantMessage = config.includeLastAssistantMessage ? lastAssistantMessage(event.messages as readonly unknown[]) : undefined;
237
+ const message = truncateMessage(assistantMessage || formatTemplate(config.message, ctx.cwd));
238
+ const result = await sendBrrr(webhook, {
239
+ title: formatTemplate(config.title, ctx.cwd),
240
+ message,
241
+ sound: config.sound.trim() || undefined,
242
+ open_url: config.openUrl.trim() || undefined,
243
+ image_url: config.imageUrl.trim() || undefined,
244
+ });
245
+
246
+ if (!result.ok && result.error) {
247
+ console.error(`brrr notification failed: ${result.error}`);
248
+ }
249
+ });
250
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@diegopetrucci/pi-brrr",
3
+ "version": "0.1.0",
4
+ "description": "A pi extension that sends brrr push notifications when pi is ready for input.",
5
+ "keywords": ["pi-package", "pi", "brrr", "notification", "push"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/diegopetrucci/pi-extensions.git",
10
+ "directory": "extensions/brrr"
11
+ },
12
+ "files": [
13
+ "index.ts",
14
+ "README.md",
15
+ "brrr.example.json"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "pi": {
21
+ "extensions": [
22
+ "index.ts"
23
+ ]
24
+ },
25
+ "peerDependencies": {
26
+ "@earendil-works/pi-coding-agent": "*"
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.32",
4
- "description": "A collection of pi extensions for context management, workflow audits, review-comment triage, notifications, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, and model/provider helpers.",
3
+ "version": "0.1.33",
4
+ "description": "A collection of pi extensions for context management, workflow audits, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, and model/provider helpers.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi",
@@ -34,6 +34,7 @@
34
34
  "pi": {
35
35
  "extensions": [
36
36
  "./extensions/agent-workflow-audit/index.ts",
37
+ "./extensions/brrr/index.ts",
37
38
  "./extensions/minimal-footer/index.ts",
38
39
  "./extensions/oracle/index.ts",
39
40
  "./extensions/context-cap/index.ts",