pi-notify 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 Warren Winter
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,64 @@
1
+ # Notifications for Pi (`pi-notify`)
2
+
3
+ Sends notifications when an agent turn finishes and took longer than a configurable threshold.
4
+
5
+ Supports:
6
+ - desktop popups (macOS)
7
+ - sounds (macOS `afplay`), with plenty of customization options
8
+ - optional Pushover notifications (useful for Apple Watch / iOS)
9
+
10
+ ## Install
11
+
12
+ From npm:
13
+
14
+ ```bash
15
+ pi install npm:pi-notify
16
+ ```
17
+
18
+ From the dot314 git bundle (filtered install):
19
+
20
+ Add to `~/.pi/agent/settings.json` (or replace an existing unfiltered `git:github.com/w-winter/dot314` entry):
21
+
22
+ ```json
23
+ {
24
+ "packages": [
25
+ {
26
+ "source": "git:github.com/w-winter/dot314",
27
+ "extensions": ["extensions/notify/index.ts"],
28
+ "skills": [],
29
+ "themes": [],
30
+ "prompts": []
31
+ }
32
+ ]
33
+ }
34
+ ```
35
+
36
+ ## Setup
37
+
38
+ Create your config file:
39
+
40
+ - copy `notify.json.example` → `notify.json`
41
+
42
+ Location:
43
+
44
+ - `~/.pi/agent/extensions/notify/notify.json`
45
+
46
+ (If the config file is missing, the extension will error with instructions.)
47
+
48
+ ## Usage
49
+
50
+ - Command: `/notify`
51
+ - Shortcut: `Alt+N` (toggle on/off)
52
+
53
+ Quick forms:
54
+ - `/notify on|off`
55
+ - `/notify popup` (toggle popup)
56
+ - `/notify pushover` (toggle Pushover)
57
+ - `/notify volume` (toggle constant ↔ timeScaled)
58
+ - `/notify <seconds>` (set minimum duration threshold)
59
+ - `/notify <sound-alias>` (set sound)
60
+
61
+ ## Notes
62
+
63
+ - macOS-only out of the box (uses `osascript` + `afplay`)
64
+ - Pushover requires `curl` and valid `userKey` + `apiToken` in config
@@ -0,0 +1,583 @@
1
+ /**
2
+ * Desktop Notification Extension
3
+ *
4
+ * Sends a native desktop notification (with optional sound) when the agent finishes,
5
+ * but only if the response took longer than a configurable threshold.
6
+ *
7
+ * Features:
8
+ * - /notify command to configure (or quick: /notify on|off|popup|pushover|<seconds>|<sound>|volume)
9
+ * - Configurable hotkey (default Alt+N) to toggle on/off
10
+ * - Only notifies if agent turn took >= minDurationSeconds
11
+ * - Configurable sounds: system sounds, custom paths, silent, or random
12
+ * - "silent" reserved alias: no sound plays (popup only if enabled)
13
+ * - "random" reserved alias: randomly picks from all sounds with paths
14
+ * - Popup and sound can be toggled independently
15
+ * - Volume modes: "constant" (always max) or "timeScaled" (louder for longer responses)
16
+ * - Pushover integration for Apple Watch / iOS notifications
17
+ * - Status indicator in footer (♫ sound, ↥ popup, ⚡︎ pushover)
18
+ *
19
+ * Configuration file: ~/.pi/agent/extensions/notify/notify.json
20
+ *
21
+ * Volume modes:
22
+ * - "constant": Always plays at volume.max
23
+ * - "timeScaled": Linear interpolation from volume.min (at threshold) to volume.max (at 4× threshold)
24
+ *
25
+ * Usage:
26
+ * - Alt+N (or configured hotkey) to toggle notifications on/off
27
+ * - /notify - open configuration menu
28
+ * - /notify on|off - toggle directly
29
+ * - /notify popup - toggle popup on/off
30
+ * - /notify pushover - toggle Pushover on/off
31
+ * - /notify volume - toggle between constant/timeScaled
32
+ * - /notify 10 - set minimum duration to 10 seconds
33
+ * - /notify glass - set sound to Glass (case-insensitive alias match)
34
+ * - /notify silent - disable sound (popup only)
35
+ * - /notify random - randomly select sound each notification
36
+ */
37
+
38
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
39
+ import { execSync, spawn } from "node:child_process";
40
+ import { homedir } from "node:os";
41
+ import { join, dirname } from "node:path";
42
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
43
+ import { Key, type KeyId } from "@mariozechner/pi-tui";
44
+
45
+ // =============================================================================
46
+ // Configuration
47
+ // =============================================================================
48
+
49
+ // Configurable hotkey - change this to your preference
50
+ // Examples: Key.ctrl("n"), Key.alt("n"), Key.ctrlShift("n")
51
+ const TOGGLE_HOTKEY: KeyId = Key.alt("n");
52
+
53
+ // =============================================================================
54
+ // Types
55
+ // =============================================================================
56
+
57
+ interface SoundEntry {
58
+ alias: string;
59
+ path?: string; // undefined for reserved aliases like "silent" and "random"
60
+ }
61
+
62
+ interface VolumeConfig {
63
+ mode: "constant" | "timeScaled";
64
+ max: number; // 0.0 to 1.0+
65
+ min: number; // 0.0 to 1.0+ (only used in timeScaled mode)
66
+ }
67
+
68
+ interface PushoverConfig {
69
+ enabled: boolean;
70
+ userKey: string;
71
+ apiToken: string;
72
+ }
73
+
74
+ interface NotifyConfig {
75
+ enabled: boolean;
76
+ minDurationSeconds: number;
77
+ sound: string; // alias reference
78
+ showPopup: boolean;
79
+ sounds: SoundEntry[];
80
+ volume: VolumeConfig;
81
+ pushover: PushoverConfig;
82
+ }
83
+
84
+ // =============================================================================
85
+ // Config File Management
86
+ // =============================================================================
87
+
88
+ function getConfigPath(): string {
89
+ return join(homedir(), ".pi", "agent", "extensions", "notify", "notify.json");
90
+ }
91
+
92
+ function loadConfig(): NotifyConfig {
93
+ const configPath = getConfigPath();
94
+
95
+ if (existsSync(configPath)) {
96
+ try {
97
+ const content = readFileSync(configPath, "utf-8");
98
+ const parsed = JSON.parse(content);
99
+
100
+ // Apply defaults for optional sections
101
+ return {
102
+ ...parsed,
103
+ volume: {
104
+ mode: "constant",
105
+ max: 1.0,
106
+ min: 0.25,
107
+ ...parsed.volume,
108
+ },
109
+ pushover: {
110
+ enabled: false,
111
+ userKey: "",
112
+ apiToken: "",
113
+ ...parsed.pushover,
114
+ },
115
+ } as NotifyConfig;
116
+ } catch {
117
+ // Fall through to error
118
+ }
119
+ }
120
+
121
+ throw new Error(
122
+ `Notify extension: config file not found at ${configPath}. ` +
123
+ `Please create it with the required structure (see extension docstring).`
124
+ );
125
+ }
126
+
127
+ function saveConfig(config: NotifyConfig): void {
128
+ const configPath = getConfigPath();
129
+ const dir = dirname(configPath);
130
+
131
+ if (!existsSync(dir)) {
132
+ mkdirSync(dir, { recursive: true });
133
+ }
134
+
135
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
136
+ }
137
+
138
+ // =============================================================================
139
+ // Volume Calculation
140
+ // =============================================================================
141
+
142
+ function calculateVolume(config: NotifyConfig, elapsedSeconds: number): number {
143
+ if (config.volume.mode === "constant") {
144
+ return config.volume.max;
145
+ }
146
+
147
+ // timeScaled mode: linear interpolation from min to max
148
+ // At 1× threshold: min volume
149
+ // At 4× threshold: max volume
150
+ const threshold = config.minDurationSeconds;
151
+ const minTime = threshold;
152
+ const maxTime = threshold * 4;
153
+
154
+ if (elapsedSeconds <= minTime) {
155
+ return config.volume.min;
156
+ }
157
+ if (elapsedSeconds >= maxTime) {
158
+ return config.volume.max;
159
+ }
160
+
161
+ // Linear interpolation
162
+ const t = (elapsedSeconds - minTime) / (maxTime - minTime);
163
+ return config.volume.min + t * (config.volume.max - config.volume.min);
164
+ }
165
+
166
+ // =============================================================================
167
+ // Sound Playback
168
+ // =============================================================================
169
+
170
+ function findSound(config: NotifyConfig, alias: string): SoundEntry | undefined {
171
+ return config.sounds.find((s) => s.alias.toLowerCase() === alias.toLowerCase());
172
+ }
173
+
174
+ function getPlayableSound(config: NotifyConfig, alias: string): SoundEntry | undefined {
175
+ const lowerAlias = alias.toLowerCase();
176
+
177
+ // Handle "random" - pick a random sound (excluding silent and random)
178
+ if (lowerAlias === "random") {
179
+ const playableSounds = config.sounds.filter(
180
+ (s) => s.path && s.alias.toLowerCase() !== "silent" && s.alias.toLowerCase() !== "random"
181
+ );
182
+ if (playableSounds.length === 0) return undefined;
183
+ return playableSounds[Math.floor(Math.random() * playableSounds.length)];
184
+ }
185
+
186
+ return findSound(config, alias);
187
+ }
188
+
189
+ function playSound(soundEntry: SoundEntry | undefined, volume: number): void {
190
+ if (!soundEntry || !soundEntry.path) {
191
+ // silent or not found
192
+ return;
193
+ }
194
+
195
+ // Spawn detached so sound plays without blocking input
196
+ const child = spawn("afplay", ["-v", String(volume), soundEntry.path], {
197
+ detached: true,
198
+ stdio: "ignore",
199
+ });
200
+ child.unref();
201
+ }
202
+
203
+ // =============================================================================
204
+ // Pushover Integration
205
+ // =============================================================================
206
+
207
+ function sendPushover(config: NotifyConfig, title: string, message: string): void {
208
+ if (!config.pushover.enabled || !config.pushover.userKey || !config.pushover.apiToken) {
209
+ return;
210
+ }
211
+
212
+ // Spawn curl detached so it doesn't block
213
+ const child = spawn("curl", [
214
+ "-s",
215
+ "-X", "POST",
216
+ "https://api.pushover.net/1/messages.json",
217
+ "--data-urlencode", `token=${config.pushover.apiToken}`,
218
+ "--data-urlencode", `user=${config.pushover.userKey}`,
219
+ "--data-urlencode", `title=${title}`,
220
+ "--data-urlencode", `message=${message}`,
221
+ ], {
222
+ detached: true,
223
+ stdio: "ignore",
224
+ });
225
+ child.unref();
226
+ }
227
+
228
+ // =============================================================================
229
+ // Notification
230
+ // =============================================================================
231
+
232
+ function notify(title: string, body: string, config: NotifyConfig, elapsedSeconds: number): void {
233
+ // Show popup notification if enabled
234
+ if (config.showPopup) {
235
+ try {
236
+ execSync(`osascript -e 'display notification "${body}" with title "${title}"'`);
237
+ } catch {
238
+ // Silently fail
239
+ }
240
+ }
241
+
242
+ // Play sound with calculated volume
243
+ const soundEntry = getPlayableSound(config, config.sound);
244
+ const volume = calculateVolume(config, elapsedSeconds);
245
+ playSound(soundEntry, volume);
246
+
247
+ // Send Pushover notification
248
+ sendPushover(config, title, body);
249
+ }
250
+
251
+ // =============================================================================
252
+ // Extension
253
+ // =============================================================================
254
+
255
+ export default function notifyExtension(pi: ExtensionAPI) {
256
+ let config: NotifyConfig;
257
+ let agentStartTime: number | null = null;
258
+
259
+ // =========================================================================
260
+ // Status Display
261
+ // =========================================================================
262
+
263
+ function updateStatus(ctx: ExtensionContext): void {
264
+ if (!ctx.hasUI) return;
265
+
266
+ if (config.enabled) {
267
+ const lowerSound = config.sound.toLowerCase();
268
+ const soundIndicator = lowerSound === "silent" ? "" : "♫";
269
+ const popupIndicator = config.showPopup ? "↑" : "";
270
+ const pushoverIndicator = config.pushover.enabled ? "⚡︎" : "";
271
+ ctx.ui.setStatus(
272
+ "notify",
273
+ ctx.ui.theme.fg("success", `${soundIndicator}${popupIndicator}${pushoverIndicator} ${config.minDurationSeconds}s`)
274
+ );
275
+ } else {
276
+ ctx.ui.setStatus("notify", ctx.ui.theme.fg("muted", ""));
277
+ }
278
+ }
279
+
280
+ // =========================================================================
281
+ // Toggle Functions
282
+ // =========================================================================
283
+
284
+ function toggleEnabled(ctx: ExtensionContext): void {
285
+ config.enabled = !config.enabled;
286
+ saveConfig(config);
287
+
288
+ if (ctx.hasUI) {
289
+ ctx.ui.notify(
290
+ config.enabled
291
+ ? `Notifications enabled (≥${config.minDurationSeconds}s)`
292
+ : "Notifications disabled",
293
+ "info"
294
+ );
295
+ }
296
+
297
+ updateStatus(ctx);
298
+ }
299
+
300
+ function togglePopup(ctx: ExtensionContext): void {
301
+ config.showPopup = !config.showPopup;
302
+ saveConfig(config);
303
+
304
+ if (ctx.hasUI) {
305
+ ctx.ui.notify(
306
+ config.showPopup ? "Popup notifications enabled" : "Popup notifications disabled",
307
+ "info"
308
+ );
309
+ }
310
+
311
+ updateStatus(ctx);
312
+ }
313
+
314
+ function togglePushover(ctx: ExtensionContext): void {
315
+ config.pushover.enabled = !config.pushover.enabled;
316
+ saveConfig(config);
317
+
318
+ if (ctx.hasUI) {
319
+ ctx.ui.notify(
320
+ config.pushover.enabled ? "Pushover notifications enabled" : "Pushover notifications disabled",
321
+ "info"
322
+ );
323
+ }
324
+
325
+ updateStatus(ctx);
326
+ }
327
+
328
+ function toggleVolumeMode(ctx: ExtensionContext): void {
329
+ config.volume.mode = config.volume.mode === "constant" ? "timeScaled" : "constant";
330
+ saveConfig(config);
331
+
332
+ if (ctx.hasUI) {
333
+ ctx.ui.notify(
334
+ config.volume.mode === "constant"
335
+ ? `Volume mode: constant (${config.volume.max})`
336
+ : `Volume mode: timeScaled (${config.volume.min} → ${config.volume.max})`,
337
+ "info"
338
+ );
339
+ }
340
+ }
341
+
342
+ // =========================================================================
343
+ // Hotkey Registration
344
+ // =========================================================================
345
+
346
+ pi.registerShortcut(TOGGLE_HOTKEY, {
347
+ description: "Toggle notifications",
348
+ handler: async (ctx) => {
349
+ toggleEnabled(ctx);
350
+ },
351
+ });
352
+
353
+ // =========================================================================
354
+ // Command Registration
355
+ // =========================================================================
356
+
357
+ pi.registerCommand("notify", {
358
+ description: "Configure desktop notifications",
359
+ handler: async (args, ctx) => {
360
+ // Quick subcommands
361
+ if (args) {
362
+ const arg = args.trim().toLowerCase();
363
+
364
+ // /notify on
365
+ if (arg === "on") {
366
+ config.enabled = true;
367
+ saveConfig(config);
368
+ ctx.ui.notify("Notifications enabled", "info");
369
+ updateStatus(ctx);
370
+ return;
371
+ }
372
+
373
+ // /notify off
374
+ if (arg === "off") {
375
+ config.enabled = false;
376
+ saveConfig(config);
377
+ ctx.ui.notify("Notifications disabled", "info");
378
+ updateStatus(ctx);
379
+ return;
380
+ }
381
+
382
+ // /notify popup
383
+ if (arg === "popup") {
384
+ togglePopup(ctx);
385
+ return;
386
+ }
387
+
388
+ // /notify pushover
389
+ if (arg === "pushover") {
390
+ togglePushover(ctx);
391
+ return;
392
+ }
393
+
394
+ // /notify volume
395
+ if (arg === "volume") {
396
+ toggleVolumeMode(ctx);
397
+ return;
398
+ }
399
+
400
+ // /notify <number> - set duration
401
+ const num = parseInt(arg, 10);
402
+ if (!isNaN(num) && num >= 0) {
403
+ config.minDurationSeconds = num;
404
+ saveConfig(config);
405
+ ctx.ui.notify(`Notification threshold set to ${num} seconds`, "info");
406
+ updateStatus(ctx);
407
+ return;
408
+ }
409
+
410
+ // /notify <sound alias> - set sound (case-insensitive match)
411
+ const matchedSound = findSound(config, arg);
412
+ if (matchedSound) {
413
+ config.sound = matchedSound.alias;
414
+ saveConfig(config);
415
+ if (matchedSound.path) {
416
+ playSound(matchedSound, config.volume.max); // Preview at max volume
417
+ }
418
+ ctx.ui.notify(`Notification sound set to ${matchedSound.alias}`, "info");
419
+ updateStatus(ctx);
420
+ return;
421
+ }
422
+
423
+ // Unknown arg - show help
424
+ ctx.ui.notify(
425
+ `Unknown argument: ${args}\nUse: on, off, popup, pushover, volume, <seconds>, or <sound alias>`,
426
+ "warning"
427
+ );
428
+ return;
429
+ }
430
+
431
+ // No args - show interactive menu
432
+ const menuItems = [
433
+ `${config.enabled ? "Disable" : "Enable"} notifications`,
434
+ `${config.showPopup ? "Disable" : "Enable"} popup`,
435
+ `${config.pushover.enabled ? "Disable" : "Enable"} Pushover (watch)`,
436
+ `Volume mode: ${config.volume.mode} (tap to toggle)`,
437
+ `Set max volume (current: ${config.volume.max})`,
438
+ ...(config.volume.mode === "timeScaled" ? [`Set min volume (current: ${config.volume.min})`] : []),
439
+ `Set duration threshold (current: ${config.minDurationSeconds}s)`,
440
+ `Change sound (current: ${config.sound})`,
441
+ "Test notification",
442
+ ];
443
+
444
+ const choice = await ctx.ui.select("Notification Settings", menuItems);
445
+
446
+ if (choice === null) return;
447
+
448
+ // Toggle notifications
449
+ if (choice === menuItems[0]) {
450
+ toggleEnabled(ctx);
451
+ return;
452
+ }
453
+
454
+ // Toggle popup
455
+ if (choice === menuItems[1]) {
456
+ togglePopup(ctx);
457
+ return;
458
+ }
459
+
460
+ // Toggle Pushover
461
+ if (choice === menuItems[2]) {
462
+ togglePushover(ctx);
463
+ return;
464
+ }
465
+
466
+ // Toggle volume mode
467
+ if (choice === menuItems[3]) {
468
+ toggleVolumeMode(ctx);
469
+ return;
470
+ }
471
+
472
+ // Set max volume
473
+ if (choice === menuItems[4]) {
474
+ const input = await ctx.ui.input("Max volume (0.0 - 1.0+)", String(config.volume.max));
475
+ if (input !== null) {
476
+ const vol = parseFloat(input);
477
+ if (!isNaN(vol) && vol >= 0) {
478
+ config.volume.max = vol;
479
+ saveConfig(config);
480
+ ctx.ui.notify(`Max volume set to ${vol}`, "info");
481
+ } else {
482
+ ctx.ui.notify("Invalid volume", "error");
483
+ }
484
+ }
485
+ return;
486
+ }
487
+
488
+ // Set min volume (only in timeScaled mode)
489
+ if (config.volume.mode === "timeScaled" && choice === menuItems[5]) {
490
+ const input = await ctx.ui.input("Min volume (0.0 - 1.0+)", String(config.volume.min));
491
+ if (input !== null) {
492
+ const vol = parseFloat(input);
493
+ if (!isNaN(vol) && vol >= 0) {
494
+ config.volume.min = vol;
495
+ saveConfig(config);
496
+ ctx.ui.notify(`Min volume set to ${vol}`, "info");
497
+ } else {
498
+ ctx.ui.notify("Invalid volume", "error");
499
+ }
500
+ }
501
+ return;
502
+ }
503
+
504
+ // Set duration - index shifts based on whether min volume is shown
505
+ const durationIndex = config.volume.mode === "timeScaled" ? 6 : 5;
506
+ if (choice === menuItems[durationIndex]) {
507
+ const input = await ctx.ui.input(
508
+ "Minimum duration (seconds)",
509
+ String(config.minDurationSeconds)
510
+ );
511
+ if (input !== null) {
512
+ const num = parseInt(input, 10);
513
+ if (!isNaN(num) && num >= 0) {
514
+ config.minDurationSeconds = num;
515
+ saveConfig(config);
516
+ ctx.ui.notify(`Threshold set to ${num} seconds`, "info");
517
+ updateStatus(ctx);
518
+ } else {
519
+ ctx.ui.notify("Invalid number", "error");
520
+ }
521
+ }
522
+ return;
523
+ }
524
+
525
+ // Change sound
526
+ const soundIndex = config.volume.mode === "timeScaled" ? 7 : 6;
527
+ if (choice === menuItems[soundIndex]) {
528
+ const soundAliases = config.sounds.map((s) => s.alias);
529
+ const soundChoice = await ctx.ui.select("Select sound", soundAliases);
530
+ if (soundChoice !== null) {
531
+ config.sound = soundChoice;
532
+ saveConfig(config);
533
+ const soundEntry = findSound(config, soundChoice);
534
+ if (soundEntry?.path) {
535
+ playSound(soundEntry, config.volume.max); // Preview at max volume
536
+ }
537
+ ctx.ui.notify(`Sound set to ${soundChoice}`, "info");
538
+ updateStatus(ctx);
539
+ }
540
+ return;
541
+ }
542
+
543
+ // Test notification
544
+ const testIndex = config.volume.mode === "timeScaled" ? 8 : 7;
545
+ if (choice === menuItems[testIndex]) {
546
+ // Test at 4x threshold to demonstrate max volume
547
+ notify("Pi", "℟", config, config.minDurationSeconds * 4);
548
+ return;
549
+ }
550
+ },
551
+ });
552
+
553
+ // =========================================================================
554
+ // Agent Lifecycle Events
555
+ // =========================================================================
556
+
557
+ pi.on("agent_start", async () => {
558
+ agentStartTime = Date.now();
559
+ });
560
+
561
+ pi.on("agent_end", async () => {
562
+ if (!config.enabled || agentStartTime === null) {
563
+ agentStartTime = null;
564
+ return;
565
+ }
566
+
567
+ const elapsedSeconds = (Date.now() - agentStartTime) / 1000;
568
+ agentStartTime = null;
569
+
570
+ if (elapsedSeconds >= config.minDurationSeconds) {
571
+ notify("Pi", "℟", config, elapsedSeconds);
572
+ }
573
+ });
574
+
575
+ // =========================================================================
576
+ // Session Initialization
577
+ // =========================================================================
578
+
579
+ pi.on("session_start", async (_event, ctx) => {
580
+ config = loadConfig();
581
+ updateStatus(ctx);
582
+ });
583
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "enabled": true,
3
+ "minDurationSeconds": 10,
4
+ "sound": "silent",
5
+ "showPopup": false,
6
+ "sounds": [
7
+ {
8
+ "alias": "silent"
9
+ },
10
+ {
11
+ "alias": "random"
12
+ },
13
+ {
14
+ "alias": "Funk",
15
+ "path": "/System/Library/Sounds/Funk.aiff"
16
+ },
17
+ {
18
+ "alias": "Glass",
19
+ "path": "/System/Library/Sounds/Glass.aiff"
20
+ },
21
+ {
22
+ "alias": "Hero",
23
+ "path": "/System/Library/Sounds/Hero.aiff"
24
+ },
25
+ {
26
+ "alias": "Submarine",
27
+ "path": "/System/Library/Sounds/Submarine.aiff"
28
+ },
29
+ {
30
+ "alias": "dubdelay1",
31
+ "path": "/Users/yourUser/Documents/notification_sounds/dubdelay1.mp3"
32
+ },
33
+ {
34
+ "alias": "dubdelay2",
35
+ "path": "/Users/yourUser/Documents/notification_sounds/dubdelay2.mp3"
36
+ },
37
+ {
38
+ "alias": "liquid",
39
+ "path": "/Users/yourUser/Documents/notification_sounds/liquid-notif.mp3"
40
+ }
41
+ ],
42
+ "volume": {
43
+ "mode": "timeScaled",
44
+ "max": 1,
45
+ "min": 0.1
46
+ },
47
+ "pushover": {
48
+ "enabled": true,
49
+ "userKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
50
+ "apiToken": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
51
+ }
52
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "pi-notify",
3
+ "version": "0.1.0",
4
+ "description": "Highly configurable desktop/sound/Pushover notifications when Pi agent turn finishes and took longer than a definable threshold",
5
+ "keywords": ["pi-package", "pi", "pi-coding-agent"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/w-winter/dot314.git",
10
+ "directory": "packages/pi-notify"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/w-winter/dot314/issues"
14
+ },
15
+ "homepage": "https://github.com/w-winter/dot314#readme",
16
+ "pi": {
17
+ "extensions": ["extensions/notify/index.ts"]
18
+ },
19
+ "peerDependencies": {
20
+ "@mariozechner/pi-coding-agent": "*",
21
+ "@mariozechner/pi-tui": "*"
22
+ },
23
+ "scripts": {
24
+ "prepack": "node ../../scripts/pi-package-prepack.mjs"
25
+ },
26
+ "files": ["extensions/**", "README.md", "LICENSE", "package.json"],
27
+ "dot314Prepack": {
28
+ "copy": [
29
+ { "from": "../../extensions/notify/index.ts", "to": "extensions/notify/index.ts" },
30
+ {
31
+ "from": "../../extensions/notify/notify.json.example",
32
+ "to": "extensions/notify/notify.json.example"
33
+ },
34
+ { "from": "../../LICENSE", "to": "LICENSE" }
35
+ ]
36
+ }
37
+ }