@diegopetrucci/pi-extensions 0.1.7 → 0.1.9

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
@@ -10,6 +10,7 @@ A collection of [pi](https://github.com/badlogic/pi-mono) agent extensions I mad
10
10
  | [`oracle`](./extensions/oracle) | Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, covers pi’s built-in providers with hardcoded rankings, sets reasoning to xhigh by default, and shows live status while running. |
11
11
  | [`permission-gate`](./extensions/permission-gate) | Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`. |
12
12
  | [`confirm-destructive`](./extensions/confirm-destructive) | Confirms before destructive session actions like clear, switch, and fork. |
13
+ | [`notify`](./extensions/notify) | Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input. |
13
14
 
14
15
  ## Install
15
16
 
@@ -24,7 +25,7 @@ pi install git:github.com/diegopetrucci/pi-extensions
24
25
  Or pin to a tagged version:
25
26
 
26
27
  ```bash
27
- pi install git:github.com/diegopetrucci/pi-extensions@v0.1.7
28
+ pi install git:github.com/diegopetrucci/pi-extensions@v0.1.9
28
29
  ```
29
30
 
30
31
  ### npm
@@ -63,6 +64,10 @@ pi install npm:@diegopetrucci/pi-permission-gate
63
64
  pi install npm:@diegopetrucci/pi-confirm-destructive
64
65
  ```
65
66
 
67
+ ```bash
68
+ pi install npm:@diegopetrucci/pi-notify
69
+ ```
70
+
66
71
  ### Option 2: filter the repo package
67
72
 
68
73
  If you prefer the collection package, you can filter it in your pi settings.
@@ -119,9 +124,22 @@ Confirm destructive only:
119
124
  }
120
125
  ```
121
126
 
127
+ Notify only:
128
+
129
+ ```json
130
+ {
131
+ "packages": [
132
+ {
133
+ "source": "npm:@diegopetrucci/pi-extensions",
134
+ "extensions": ["extensions/notify/index.ts"]
135
+ }
136
+ ]
137
+ }
138
+ ```
139
+
122
140
  ## npm publishing
123
141
 
124
142
  The repo is set up to support both:
125
143
 
126
144
  - the collection package: `@diegopetrucci/pi-extensions`
127
- - standalone extension packages like `@diegopetrucci/pi-minimal-footer`, `@diegopetrucci/pi-oracle`, `@diegopetrucci/pi-permission-gate`, and `@diegopetrucci/pi-confirm-destructive`
145
+ - standalone extension packages like `@diegopetrucci/pi-minimal-footer`, `@diegopetrucci/pi-oracle`, `@diegopetrucci/pi-permission-gate`, `@diegopetrucci/pi-confirm-destructive`, and `@diegopetrucci/pi-notify`
@@ -0,0 +1,125 @@
1
+ # notify
2
+
3
+ A pi extension that sends notifications when the agent finishes and is waiting for input.
4
+
5
+ This started from the original `notify.ts` example in [`badlogic/pi-mono`](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/examples/extensions/notify.ts), but now supports multiple notification channels and JSON configuration.
6
+
7
+ ## Supported notification channels
8
+
9
+ ### Terminal notifications
10
+
11
+ - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode
12
+ - OSC 99: Kitty
13
+
14
+ ### Desktop notifications
15
+
16
+ - macOS Notification Center via `osascript`
17
+ - Linux desktop notifications via `notify-send`
18
+ - Windows toast notifications via `powershell.exe` / Windows Terminal / WSL
19
+
20
+ ### Bells and sounds
21
+
22
+ - terminal bell (`\a`)
23
+ - macOS sound playback via `afplay`
24
+ - Linux sound playback via `canberra-gtk-play` or `paplay`
25
+ - Windows beep via `powershell.exe`
26
+
27
+ By default, all channels are enabled:
28
+
29
+ - terminal notification
30
+ - desktop notification
31
+ - bell
32
+ - sound
33
+
34
+ The extension automatically picks the appropriate backend for the current environment.
35
+
36
+ ## Install
37
+
38
+ ### Standalone npm package
39
+
40
+ ```bash
41
+ pi install npm:@diegopetrucci/pi-notify
42
+ ```
43
+
44
+ ### Collection package
45
+
46
+ ```bash
47
+ pi install npm:@diegopetrucci/pi-extensions
48
+ ```
49
+
50
+ ### GitHub package
51
+
52
+ ```bash
53
+ pi install git:github.com/diegopetrucci/pi-extensions
54
+ ```
55
+
56
+ Then reload pi:
57
+
58
+ ```text
59
+ /reload
60
+ ```
61
+
62
+ ## Configuration
63
+
64
+ Config files are merged, with project config overriding global config:
65
+
66
+ - `~/.pi/agent/extensions/notify.json`
67
+ - `<project>/.pi/notify.json`
68
+
69
+ A ready-to-copy sample file is included at [`notify.example.json`](./notify.example.json).
70
+
71
+ Example:
72
+
73
+ ```json
74
+ {
75
+ "enabled": true,
76
+ "onlyWhenInteractive": true,
77
+ "title": "Pi",
78
+ "body": "Ready for input",
79
+ "channels": {
80
+ "terminal": true,
81
+ "desktop": true,
82
+ "bell": true,
83
+ "sound": true
84
+ },
85
+ "terminal": {
86
+ "backend": "auto"
87
+ },
88
+ "desktop": {
89
+ "backend": "auto"
90
+ },
91
+ "sound": {
92
+ "backend": "auto",
93
+ "name": "Glass",
94
+ "linuxSoundId": "complete",
95
+ "frequencyHz": 1000,
96
+ "durationMs": 250,
97
+ "command": ""
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Config fields
103
+
104
+ - `enabled`: master on/off switch
105
+ - `onlyWhenInteractive`: skip notifications in print / non-UI mode
106
+ - `title`: notification title
107
+ - `body`: notification body
108
+ - `channels.terminal`: enable terminal notification output
109
+ - `channels.desktop`: enable OS desktop notifications
110
+ - `channels.bell`: enable terminal bell
111
+ - `channels.sound`: enable sound playback
112
+ - `terminal.backend`: `auto`, `osc777`, `osc99`, `none`
113
+ - `desktop.backend`: `auto`, `macos`, `linux`, `windows-toast`, `none`
114
+ - `sound.backend`: `auto`, `macos`, `linux`, `windows-beep`, `command`, `none`
115
+ - `sound.name`: macOS system sound name, like `Glass` or `Hero`
116
+ - `sound.linuxSoundId`: freedesktop sound id, like `complete`
117
+ - `sound.frequencyHz`: Windows beep frequency
118
+ - `sound.durationMs`: Windows beep duration
119
+ - `sound.command`: custom shell command when `sound.backend` is `command`
120
+
121
+ ## Notes
122
+
123
+ - Hooks the `agent_end` event.
124
+ - Default message is `Pi` / `Ready for input`.
125
+ - Terminal, desktop, bell, and sound channels can be enabled independently.
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Pi Notify Extension
3
+ *
4
+ * Sends notifications when Pi agent is done and waiting for input.
5
+ * Supports multiple channels:
6
+ * - terminal notifications: OSC 777 and OSC 99
7
+ * - desktop notifications: macOS Notification Center, Linux notify-send, Windows toast
8
+ * - terminal bell
9
+ * - sound playback
10
+ *
11
+ * Config files (project overrides global):
12
+ * - ~/.pi/agent/extensions/notify.json
13
+ * - <cwd>/.pi/notify.json
14
+ */
15
+
16
+ import { execFile } from "node:child_process";
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
20
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
21
+
22
+ type TerminalBackend = "auto" | "osc777" | "osc99" | "none";
23
+ type DesktopBackend = "auto" | "macos" | "linux" | "windows-toast" | "none";
24
+ type SoundBackend = "auto" | "macos" | "linux" | "windows-beep" | "command" | "none";
25
+
26
+ interface NotifyConfig {
27
+ enabled: boolean;
28
+ onlyWhenInteractive: boolean;
29
+ title: string;
30
+ body: string;
31
+ channels: {
32
+ terminal: boolean;
33
+ desktop: boolean;
34
+ bell: boolean;
35
+ sound: boolean;
36
+ };
37
+ terminal: {
38
+ backend: TerminalBackend;
39
+ };
40
+ desktop: {
41
+ backend: DesktopBackend;
42
+ };
43
+ sound: {
44
+ backend: SoundBackend;
45
+ name: string;
46
+ linuxSoundId: string;
47
+ frequencyHz: number;
48
+ durationMs: number;
49
+ command: string;
50
+ };
51
+ }
52
+
53
+ const DEFAULT_CONFIG: NotifyConfig = {
54
+ enabled: true,
55
+ onlyWhenInteractive: true,
56
+ title: "Pi",
57
+ body: "Ready for input",
58
+ channels: {
59
+ terminal: true,
60
+ desktop: true,
61
+ bell: true,
62
+ sound: true,
63
+ },
64
+ terminal: {
65
+ backend: "auto",
66
+ },
67
+ desktop: {
68
+ backend: "auto",
69
+ },
70
+ sound: {
71
+ backend: "auto",
72
+ name: "Glass",
73
+ linuxSoundId: "complete",
74
+ frequencyHz: 1000,
75
+ durationMs: 250,
76
+ command: "",
77
+ },
78
+ };
79
+
80
+ function readConfigFile(path: string): Partial<NotifyConfig> {
81
+ if (!existsSync(path)) return {};
82
+
83
+ try {
84
+ return JSON.parse(readFileSync(path, "utf-8")) as Partial<NotifyConfig>;
85
+ } catch (error) {
86
+ console.error(`Warning: Could not parse ${path}: ${error}`);
87
+ return {};
88
+ }
89
+ }
90
+
91
+ function mergeConfig(base: NotifyConfig, overrides: Partial<NotifyConfig>): NotifyConfig {
92
+ return {
93
+ ...base,
94
+ ...overrides,
95
+ channels: {
96
+ ...base.channels,
97
+ ...overrides.channels,
98
+ },
99
+ terminal: {
100
+ ...base.terminal,
101
+ ...overrides.terminal,
102
+ },
103
+ desktop: {
104
+ ...base.desktop,
105
+ ...overrides.desktop,
106
+ },
107
+ sound: {
108
+ ...base.sound,
109
+ ...overrides.sound,
110
+ },
111
+ };
112
+ }
113
+
114
+ function loadConfig(cwd: string): NotifyConfig {
115
+ const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "notify.json"));
116
+ const projectConfig = readConfigFile(join(cwd, ".pi", "notify.json"));
117
+ return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
118
+ }
119
+
120
+ function powershellString(value: string): string {
121
+ return `'${value.replace(/'/g, "''")}'`;
122
+ }
123
+
124
+ function windowsToastScript(title: string, body: string): string {
125
+ const type = "Windows.UI.Notifications";
126
+ const mgr = `[${type}.ToastNotificationManager, ${type}, ContentType = WindowsRuntime]`;
127
+ const template = `[${type}.ToastTemplateType]::ToastText01`;
128
+ const toast = `[${type}.ToastNotification]::new($xml)`;
129
+ return [
130
+ `${mgr} > $null`,
131
+ `$xml = [${type}.ToastNotificationManager]::GetTemplateContent(${template})`,
132
+ `$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode(${powershellString(body)})) > $null`,
133
+ `[${type}.ToastNotificationManager]::CreateToastNotifier(${powershellString(title)}).Show(${toast})`,
134
+ ].join("; ");
135
+ }
136
+
137
+ function notifyOSC777(title: string, body: string): void {
138
+ process.stdout.write(`\x1b]777;notify;${title};${body}\x07`);
139
+ }
140
+
141
+ function notifyOSC99(title: string, body: string): void {
142
+ process.stdout.write(`\x1b]99;i=1:d=0;${title}\x1b\\`);
143
+ process.stdout.write(`\x1b]99;i=1:p=body;${body}\x1b\\`);
144
+ }
145
+
146
+ function ringBell(): void {
147
+ process.stdout.write("\x07");
148
+ }
149
+
150
+ function runCommand(command: string, args: string[]): Promise<boolean> {
151
+ return new Promise((resolve) => {
152
+ execFile(command, args, (error) => resolve(!error));
153
+ });
154
+ }
155
+
156
+ function runShellCommand(command: string): Promise<boolean> {
157
+ if (process.platform === "win32") {
158
+ return runCommand("cmd.exe", ["/d", "/s", "/c", command]);
159
+ }
160
+
161
+ return runCommand(process.env.SHELL || "/bin/sh", ["-lc", command]);
162
+ }
163
+
164
+ function detectTerminalBackend(config: NotifyConfig): Exclude<TerminalBackend, "auto"> {
165
+ if (config.terminal.backend !== "auto") return config.terminal.backend;
166
+ if (process.env.KITTY_WINDOW_ID) return "osc99";
167
+ return "osc777";
168
+ }
169
+
170
+ function detectDesktopBackend(config: NotifyConfig): Exclude<DesktopBackend, "auto"> {
171
+ if (config.desktop.backend !== "auto") return config.desktop.backend;
172
+ if (process.env.WT_SESSION || process.env.WSL_DISTRO_NAME) return "windows-toast";
173
+ if (process.platform === "darwin") return "macos";
174
+ if (process.platform === "linux") return "linux";
175
+ if (process.platform === "win32") return "windows-toast";
176
+ return "none";
177
+ }
178
+
179
+ function detectSoundBackend(config: NotifyConfig): Exclude<SoundBackend, "auto"> {
180
+ if (config.sound.backend !== "auto") return config.sound.backend;
181
+ if (process.env.WT_SESSION || process.platform === "win32" || process.env.WSL_DISTRO_NAME) return "windows-beep";
182
+ if (process.platform === "darwin") return "macos";
183
+ if (process.platform === "linux") return "linux";
184
+ return "none";
185
+ }
186
+
187
+ function sendTerminalNotification(title: string, body: string, backend: Exclude<TerminalBackend, "auto">): void {
188
+ if (backend === "osc99") {
189
+ notifyOSC99(title, body);
190
+ return;
191
+ }
192
+ if (backend === "osc777") {
193
+ notifyOSC777(title, body);
194
+ }
195
+ }
196
+
197
+ function appleScriptString(value: string): string {
198
+ return JSON.stringify(value);
199
+ }
200
+
201
+ function sendDesktopNotification(
202
+ title: string,
203
+ body: string,
204
+ backend: Exclude<DesktopBackend, "auto">,
205
+ ): Promise<boolean> {
206
+ if (backend === "windows-toast") {
207
+ return runCommand("powershell.exe", ["-NoProfile", "-Command", windowsToastScript(title, body)]);
208
+ }
209
+ if (backend === "macos") {
210
+ return runCommand("osascript", ["-e", `display notification ${appleScriptString(body)} with title ${appleScriptString(title)}`]);
211
+ }
212
+ if (backend === "linux") {
213
+ return runCommand("notify-send", [title, body]);
214
+ }
215
+ return Promise.resolve(false);
216
+ }
217
+
218
+ async function playSound(config: NotifyConfig, backend: Exclude<SoundBackend, "auto">): Promise<boolean> {
219
+ if (backend === "command") {
220
+ if (!config.sound.command.trim()) return false;
221
+ return runShellCommand(config.sound.command);
222
+ }
223
+
224
+ if (backend === "windows-beep") {
225
+ return runCommand("powershell.exe", [
226
+ "-NoProfile",
227
+ "-Command",
228
+ `[console]::beep(${config.sound.frequencyHz}, ${config.sound.durationMs})`,
229
+ ]);
230
+ }
231
+
232
+ if (backend === "macos") {
233
+ return runCommand("afplay", [`/System/Library/Sounds/${config.sound.name}.aiff`]);
234
+ }
235
+
236
+ if (backend === "linux") {
237
+ const soundId = config.sound.linuxSoundId;
238
+ const viaCanberra = await runCommand("canberra-gtk-play", ["-i", soundId]);
239
+ if (viaCanberra) return true;
240
+ return runCommand("paplay", [`/usr/share/sounds/freedesktop/stereo/${soundId}.oga`]);
241
+ }
242
+
243
+ return false;
244
+ }
245
+
246
+ export default function notifyExtension(pi: ExtensionAPI) {
247
+ pi.on("agent_end", async (_event, ctx) => {
248
+ const config = loadConfig(ctx.cwd);
249
+ if (!config.enabled) return;
250
+ if (config.onlyWhenInteractive && !ctx.hasUI) return;
251
+
252
+ const tasks: Array<Promise<unknown>> = [];
253
+
254
+ if (config.channels.terminal) {
255
+ sendTerminalNotification(config.title, config.body, detectTerminalBackend(config));
256
+ }
257
+
258
+ if (config.channels.desktop) {
259
+ tasks.push(sendDesktopNotification(config.title, config.body, detectDesktopBackend(config)));
260
+ }
261
+
262
+ if (config.channels.bell) {
263
+ ringBell();
264
+ }
265
+
266
+ if (config.channels.sound) {
267
+ tasks.push(playSound(config, detectSoundBackend(config)));
268
+ }
269
+
270
+ if (tasks.length > 0) {
271
+ await Promise.allSettled(tasks);
272
+ }
273
+ });
274
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "enabled": true,
3
+ "onlyWhenInteractive": true,
4
+ "title": "Pi",
5
+ "body": "Ready for input",
6
+ "channels": {
7
+ "terminal": true,
8
+ "desktop": true,
9
+ "bell": true,
10
+ "sound": true
11
+ },
12
+ "terminal": {
13
+ "backend": "auto"
14
+ },
15
+ "desktop": {
16
+ "backend": "auto"
17
+ },
18
+ "sound": {
19
+ "backend": "auto",
20
+ "name": "Glass",
21
+ "linuxSoundId": "complete",
22
+ "frequencyHz": 1000,
23
+ "durationMs": 250,
24
+ "command": ""
25
+ }
26
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@diegopetrucci/pi-notify",
3
+ "version": "0.1.1",
4
+ "description": "A pi extension that sends a notification when the agent is ready for input.",
5
+ "keywords": ["pi-package", "pi", "notification", "terminal"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/diegopetrucci/pi-extensions.git",
10
+ "directory": "extensions/notify"
11
+ },
12
+ "files": [
13
+ "index.ts",
14
+ "README.md",
15
+ "notify.example.json"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "pi": {
21
+ "extensions": [
22
+ "index.ts"
23
+ ]
24
+ },
25
+ "peerDependencies": {
26
+ "@mariozechner/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.7",
4
- "description": "A collection of pi extensions, including a minimal custom footer, an Amp-style oracle, a permission gate for dangerous bash commands, and confirm-before-destructive session actions.",
3
+ "version": "0.1.9",
4
+ "description": "A collection of pi extensions, including a minimal custom footer, an Amp-style oracle, a permission gate for dangerous bash commands, confirm-before-destructive session actions, and terminal notifications when pi is ready for input.",
5
5
  "keywords": ["pi-package", "pi", "terminal", "agent"],
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -29,7 +29,8 @@
29
29
  "./extensions/minimal-footer/index.ts",
30
30
  "./extensions/oracle/index.ts",
31
31
  "./extensions/permission-gate/index.ts",
32
- "./extensions/confirm-destructive/index.ts"
32
+ "./extensions/confirm-destructive/index.ts",
33
+ "./extensions/notify/index.ts"
33
34
  ],
34
35
  "image": "https://raw.githubusercontent.com/diegopetrucci/pi-extensions/main/assets/oracle-preview.svg"
35
36
  }