@pi-unipi/notify 0.1.1

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 ADDED
@@ -0,0 +1,103 @@
1
+ # @pi-unipi/notify
2
+
3
+ Cross-platform notification extension for Pi. Sends push notifications to native OS, Gotify, and Telegram when agent lifecycle events occur.
4
+
5
+ ## What it does
6
+
7
+ - Listens to Pi lifecycle events (workflow complete, Ralph loop done, MCP errors, etc.)
8
+ - Routes notifications to your configured platforms
9
+ - Provides `notify_user` tool for agent-initiated notifications
10
+ - Per-event platform configuration with sensible defaults
11
+
12
+ ## Installation
13
+
14
+ Part of the `@pi-unipi/unipi` meta-package. No separate install needed.
15
+
16
+ ## Platforms
17
+
18
+ ### Native OS (default)
19
+
20
+ Desktop notifications via [node-notifier](https://github.com/mikaelbr/node-notifier):
21
+ - **Windows:** SnoreToast (no admin required)
22
+ - **macOS:** terminal-notifier
23
+ - **Linux:** notify-send / libnotify
24
+
25
+ Zero configuration — works out of the box.
26
+
27
+ ### Gotify
28
+
29
+ Self-hosted push notification server. Configure in settings:
30
+
31
+ ```json
32
+ {
33
+ "gotify": {
34
+ "enabled": true,
35
+ "serverUrl": "https://your-gotify-server.com",
36
+ "appToken": "your-app-token",
37
+ "priority": 5
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### Telegram
43
+
44
+ Bot API notifications. Run setup command:
45
+
46
+ ```
47
+ /unipi:notify-set-tg
48
+ ```
49
+
50
+ This guides you through:
51
+ 1. Creating a bot via @BotFather
52
+ 2. Pasting the bot token
53
+ 3. Auto-detecting your chat ID
54
+
55
+ ## Commands
56
+
57
+ | Command | Description |
58
+ |---------|-------------|
59
+ | `/unipi:notify-settings` | Open settings overlay to configure platforms and events |
60
+ | `/unipi:notify-set-tg` | Interactive Telegram bot setup |
61
+ | `/unipi:notify-test` | Send test notification to all enabled platforms |
62
+
63
+ ## Agent Tool
64
+
65
+ The `notify_user` tool is available to the agent for ad-hoc notifications:
66
+
67
+ ```
68
+ notify_user({
69
+ title: "Build Failed",
70
+ message: "TypeScript compilation failed with 12 errors.",
71
+ priority: "high"
72
+ })
73
+ ```
74
+
75
+ See the bundled `notify` skill for full parameter documentation.
76
+
77
+ ## Event Configuration
78
+
79
+ Notifications are triggered by these events (configurable in settings):
80
+
81
+ | Event | Default | Description |
82
+ |-------|---------|-------------|
83
+ | `workflow_end` | On | Workflow command completes |
84
+ | `ralph_loop_end` | On | Ralph loop completes |
85
+ | `mcp_server_error` | On | MCP server error |
86
+ | `agent_end` | Off | Agent finishes responding |
87
+ | `memory_consolidated` | Off | Memory auto-saved |
88
+ | `session_shutdown` | Off | Session ends |
89
+
90
+ ## Configuration
91
+
92
+ Settings stored at `~/.unipi/config/notify/config.json`. Edit via:
93
+ - Settings overlay: `/unipi:notify-settings`
94
+ - Manual JSON editing
95
+ - The agent can read config via the settings module
96
+
97
+ ## Info-Screen Integration
98
+
99
+ The notify module registers with the info screen showing:
100
+ - Enabled platform count
101
+ - Active event subscriptions
102
+ - Last notification timestamp
103
+ - Total notifications sent this session
package/commands.ts ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @pi-unipi/notify — Command registration
3
+ *
4
+ * Registers slash commands for notification configuration and testing.
5
+ */
6
+
7
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
8
+ import { UNIPI_PREFIX } from "@pi-unipi/core";
9
+ import { NOTIFY_COMMANDS } from "@pi-unipi/core";
10
+ import { NotifySettingsOverlay } from "./tui/settings-overlay.js";
11
+ import { GotifySetupOverlay } from "./tui/gotify-setup.js";
12
+ import { TelegramSetupOverlay } from "./tui/telegram-setup.js";
13
+ import { loadConfig } from "./settings.js";
14
+ import { sendNativeNotification } from "./platforms/native.js";
15
+ import { sendGotifyNotification } from "./platforms/gotify.js";
16
+ import { sendTelegramNotification } from "./platforms/telegram.js";
17
+
18
+ /**
19
+ * Register notify commands.
20
+ */
21
+ export function registerNotifyCommands(pi: ExtensionAPI): void {
22
+ // /unipi:notify-settings — Opens settings TUI overlay
23
+ pi.registerCommand(
24
+ `${UNIPI_PREFIX}${NOTIFY_COMMANDS.SETTINGS}`,
25
+ {
26
+ description: "Configure notification platforms and events",
27
+ handler: async (_args: string, ctx: ExtensionContext) => {
28
+ if (!ctx.hasUI) {
29
+ ctx.ui.notify("Settings require an interactive UI.", "warning");
30
+ return;
31
+ }
32
+
33
+ ctx.ui.custom(
34
+ (tui: any, theme: any, _keybindings: any, done: any) => {
35
+ const overlay = new NotifySettingsOverlay();
36
+ overlay.setTheme(theme);
37
+ overlay.onClose = () => done(undefined);
38
+ overlay.requestRender = () => tui.requestRender();
39
+ return {
40
+ render: (w: number) => overlay.render(w),
41
+ invalidate: () => overlay.invalidate(),
42
+ handleInput: (data: string) => {
43
+ overlay.handleInput(data);
44
+ tui.requestRender();
45
+ },
46
+ };
47
+ },
48
+ {
49
+ overlay: true,
50
+ overlayOptions: {
51
+ width: "80%",
52
+ minWidth: 60,
53
+ anchor: "center",
54
+ margin: 2,
55
+ },
56
+ }
57
+ );
58
+ },
59
+ }
60
+ );
61
+
62
+ // /unipi:notify-set-gotify — Interactive Gotify setup
63
+ pi.registerCommand(
64
+ `${UNIPI_PREFIX}${NOTIFY_COMMANDS.SET_GOTIFY}`,
65
+ {
66
+ description: "Set up Gotify push notifications with connection test",
67
+ handler: async (_args: string, ctx: ExtensionContext) => {
68
+ if (!ctx.hasUI) {
69
+ ctx.ui.notify("Gotify setup requires an interactive UI.", "warning");
70
+ return;
71
+ }
72
+
73
+ ctx.ui.custom(
74
+ (tui: any, theme: any, _keybindings: any, done: any) => {
75
+ const overlay = new GotifySetupOverlay();
76
+ overlay.setTheme(theme);
77
+ overlay.onClose = () => done(undefined);
78
+ overlay.requestRender = () => tui.requestRender();
79
+ return {
80
+ render: (w: number) => overlay.render(w),
81
+ invalidate: () => overlay.invalidate(),
82
+ handleInput: (data: string) => {
83
+ overlay.handleInput(data);
84
+ tui.requestRender();
85
+ },
86
+ };
87
+ },
88
+ {
89
+ overlay: true,
90
+ overlayOptions: {
91
+ width: "80%",
92
+ minWidth: 60,
93
+ anchor: "center",
94
+ margin: 2,
95
+ },
96
+ }
97
+ );
98
+ },
99
+ }
100
+ );
101
+
102
+ // /unipi:notify-set-tg — Interactive Telegram setup
103
+ pi.registerCommand(
104
+ `${UNIPI_PREFIX}${NOTIFY_COMMANDS.SET_TG}`,
105
+ {
106
+ description: "Set up Telegram bot notifications with auto-detection",
107
+ handler: async (_args: string, ctx: ExtensionContext) => {
108
+ if (!ctx.hasUI) {
109
+ ctx.ui.notify("Telegram setup requires an interactive UI.", "warning");
110
+ return;
111
+ }
112
+
113
+ ctx.ui.custom(
114
+ (tui: any, theme: any, _keybindings: any, done: any) => {
115
+ const overlay = new TelegramSetupOverlay();
116
+ overlay.setTheme(theme);
117
+ overlay.onClose = () => done(undefined);
118
+ overlay.requestRender = () => tui.requestRender();
119
+ return {
120
+ render: (w: number) => overlay.render(w),
121
+ invalidate: () => overlay.invalidate(),
122
+ handleInput: (data: string) => {
123
+ overlay.handleInput(data);
124
+ tui.requestRender();
125
+ },
126
+ };
127
+ },
128
+ {
129
+ overlay: true,
130
+ overlayOptions: {
131
+ width: "80%",
132
+ minWidth: 60,
133
+ anchor: "center",
134
+ margin: 2,
135
+ },
136
+ }
137
+ );
138
+ },
139
+ }
140
+ );
141
+
142
+ // /unipi:notify-test — Send test notification to all enabled platforms
143
+ pi.registerCommand(
144
+ `${UNIPI_PREFIX}${NOTIFY_COMMANDS.TEST}`,
145
+ {
146
+ description: "Send a test notification to all enabled platforms",
147
+ handler: async (_args: string, ctx: ExtensionContext) => {
148
+ const config = loadConfig();
149
+ const title = "Pi — Test Notification";
150
+ const message = `Test notification sent at ${new Date().toLocaleTimeString()}`;
151
+ const results: string[] = [];
152
+
153
+ // Native
154
+ if (config.native.enabled) {
155
+ try {
156
+ await sendNativeNotification(title, message, {
157
+ windowsAppId: config.native.windowsAppId,
158
+ });
159
+ results.push("✓ Native: sent");
160
+ } catch (err) {
161
+ results.push(
162
+ `✗ Native: ${err instanceof Error ? err.message : "failed"}`
163
+ );
164
+ }
165
+ }
166
+
167
+ // Gotify
168
+ if (config.gotify.enabled && config.gotify.serverUrl && config.gotify.appToken) {
169
+ try {
170
+ await sendGotifyNotification(
171
+ config.gotify.serverUrl,
172
+ config.gotify.appToken,
173
+ title,
174
+ message,
175
+ config.gotify.priority
176
+ );
177
+ results.push("✓ Gotify: sent");
178
+ } catch (err) {
179
+ results.push(
180
+ `✗ Gotify: ${err instanceof Error ? err.message : "failed"}`
181
+ );
182
+ }
183
+ }
184
+
185
+ // Telegram
186
+ if (config.telegram.enabled && config.telegram.botToken && config.telegram.chatId) {
187
+ try {
188
+ await sendTelegramNotification(
189
+ config.telegram.botToken,
190
+ config.telegram.chatId,
191
+ title,
192
+ message
193
+ );
194
+ results.push("✓ Telegram: sent");
195
+ } catch (err) {
196
+ results.push(
197
+ `✗ Telegram: ${err instanceof Error ? err.message : "failed"}`
198
+ );
199
+ }
200
+ }
201
+
202
+ if (results.length === 0) {
203
+ ctx.ui.notify("No platforms enabled. Use /unipi:notify-settings first.", "warning");
204
+ } else {
205
+ ctx.ui.notify(`Test results:\n${results.join("\n")}`, "info");
206
+ }
207
+ },
208
+ }
209
+ );
210
+ }
package/events.ts ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @pi-unipi/notify — Event subscription registry
3
+ *
4
+ * Maps pi lifecycle events to notification dispatch.
5
+ * Supports built-in events and dynamic discovery via MODULE_READY.
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import { UNIPI_EVENTS, emitEvent } from "@pi-unipi/core";
10
+ import type { NotifyConfig, NotifyPlatform, NotifyDispatchResult } from "./types.js";
11
+ import { sendNativeNotification } from "./platforms/native.js";
12
+ import { sendGotifyNotification } from "./platforms/gotify.js";
13
+ import { sendTelegramNotification } from "./platforms/telegram.js";
14
+ import { loadConfig } from "./settings.js";
15
+
16
+ /** Built-in event definitions — maps event key to pi hook + display label */
17
+ export const BUILTIN_EVENTS: Record<
18
+ string,
19
+ { hook: string; label: string }
20
+ > = {
21
+ agent_end: { hook: "agent_end", label: "Agent Complete" },
22
+ workflow_end: { hook: UNIPI_EVENTS.WORKFLOW_END, label: "Workflow Done" },
23
+ ralph_loop_end: { hook: UNIPI_EVENTS.RALPH_LOOP_END, label: "Ralph Complete" },
24
+ mcp_server_error: { hook: UNIPI_EVENTS.MCP_SERVER_ERROR, label: "MCP Error" },
25
+ memory_consolidated: { hook: UNIPI_EVENTS.MEMORY_CONSOLIDATED, label: "Memory Saved" },
26
+ session_shutdown: { hook: "session_shutdown", label: "Session End" },
27
+ };
28
+
29
+ /** Stores registered listener cleanup functions */
30
+ const cleanupFns: Array<() => void> = [];
31
+
32
+ /**
33
+ * Register event listeners for all enabled notification events.
34
+ * Attaches listeners to pi hooks and routes notifications to platforms.
35
+ */
36
+ export function registerEventListeners(
37
+ pi: ExtensionAPI,
38
+ config: NotifyConfig
39
+ ): void {
40
+ // Register built-in events
41
+ for (const [eventKey, def] of Object.entries(BUILTIN_EVENTS)) {
42
+ const eventConfig = config.events[eventKey];
43
+ if (!eventConfig?.enabled) continue;
44
+
45
+ const handler = async (payload: unknown) => {
46
+ const title = `Pi — ${def.label}`;
47
+ const message = buildEventMessage(eventKey, payload);
48
+ await dispatchNotification(pi, title, message, eventConfig.platforms, eventKey, config);
49
+ };
50
+
51
+ (pi as any).on(def.hook, handler);
52
+ cleanupFns.push(() => {
53
+ (pi as any).off(def.hook, handler);
54
+ });
55
+ }
56
+
57
+ // Listen for dynamic module events
58
+ const moduleHandler = async (payload: unknown) => {
59
+ const modPayload = payload as { name?: string; tools?: string[] };
60
+ if (modPayload?.name && modPayload.name !== "@pi-unipi/notify") {
61
+ // Module announced — check if it has events we should subscribe to
62
+ // For now, modules register their own events through MODULE_READY
63
+ }
64
+ };
65
+ (pi as any).on(UNIPI_EVENTS.MODULE_READY, moduleHandler);
66
+ cleanupFns.push(() => {
67
+ (pi as any).off(UNIPI_EVENTS.MODULE_READY, moduleHandler);
68
+ });
69
+ }
70
+
71
+ /** Remove all registered event listeners */
72
+ export function unregisterEventListeners(): void {
73
+ for (const cleanup of cleanupFns) {
74
+ cleanup();
75
+ }
76
+ cleanupFns.length = 0;
77
+ }
78
+
79
+ /**
80
+ * Dispatch a notification to the configured platforms.
81
+ * Sends to all specified platforms (or defaults) in parallel.
82
+ */
83
+ export async function dispatchNotification(
84
+ pi: ExtensionAPI,
85
+ title: string,
86
+ message: string,
87
+ eventPlatforms: NotifyPlatform[],
88
+ eventType: string,
89
+ config: NotifyConfig
90
+ ): Promise<NotifyDispatchResult> {
91
+ const platforms =
92
+ eventPlatforms.length > 0 ? eventPlatforms : config.defaultPlatforms;
93
+
94
+ const enabledPlatforms = platforms.filter((p) => {
95
+ if (p === "native") return config.native.enabled;
96
+ if (p === "gotify") return config.gotify.enabled;
97
+ if (p === "telegram") return config.telegram.enabled;
98
+ return false;
99
+ });
100
+
101
+ const results = await Promise.all(
102
+ enabledPlatforms.map(async (platform) => {
103
+ try {
104
+ await sendToPlatform(platform, title, message, config);
105
+ return { platform, success: true };
106
+ } catch (err) {
107
+ console.error(
108
+ `[notify] Failed to send via ${platform}:`,
109
+ err instanceof Error ? err.message : err
110
+ );
111
+ return {
112
+ platform,
113
+ success: false,
114
+ error: err instanceof Error ? err.message : String(err),
115
+ };
116
+ }
117
+ })
118
+ );
119
+
120
+ const allSuccess = results.length > 0 && results.every((r) => r.success);
121
+
122
+ // Emit notification sent event
123
+ emitEvent(pi, UNIPI_EVENTS.NOTIFICATION_SENT, {
124
+ eventType,
125
+ platforms: enabledPlatforms,
126
+ success: allSuccess,
127
+ timestamp: new Date().toISOString(),
128
+ });
129
+
130
+ return { results, allSuccess };
131
+ }
132
+
133
+ /** Send to a single platform */
134
+ async function sendToPlatform(
135
+ platform: NotifyPlatform,
136
+ title: string,
137
+ message: string,
138
+ config: NotifyConfig
139
+ ): Promise<void> {
140
+ switch (platform) {
141
+ case "native":
142
+ await sendNativeNotification(title, message, {
143
+ windowsAppId: config.native.windowsAppId,
144
+ });
145
+ break;
146
+ case "gotify":
147
+ if (!config.gotify.serverUrl || !config.gotify.appToken) {
148
+ throw new Error("Gotify: serverUrl and appToken are required");
149
+ }
150
+ await sendGotifyNotification(
151
+ config.gotify.serverUrl,
152
+ config.gotify.appToken,
153
+ title,
154
+ message,
155
+ config.gotify.priority
156
+ );
157
+ break;
158
+ case "telegram":
159
+ if (!config.telegram.botToken || !config.telegram.chatId) {
160
+ throw new Error("Telegram: botToken and chatId are required");
161
+ }
162
+ await sendTelegramNotification(
163
+ config.telegram.botToken,
164
+ config.telegram.chatId,
165
+ title,
166
+ message
167
+ );
168
+ break;
169
+ }
170
+ }
171
+
172
+ /** Build notification message from event key and payload */
173
+ function buildEventMessage(eventKey: string, payload: unknown): string {
174
+ const p = payload as Record<string, unknown>;
175
+
176
+ switch (eventKey) {
177
+ case "workflow_end":
178
+ return `Workflow ${String(p.command || "unknown")}${p.success === false ? " failed" : " completed"}`;
179
+ case "ralph_loop_end":
180
+ return `Ralph loop "${String(p.name || "unknown")}" ${p.status || "completed"}`;
181
+ case "mcp_server_error":
182
+ return `Server "${String(p.name || "unknown")}" error: ${String(p.error || "unknown error")}`;
183
+ case "agent_end":
184
+ return "Agent finished responding";
185
+ case "memory_consolidated":
186
+ return `Memory consolidated (${p.count || 0} items)`;
187
+ case "session_shutdown":
188
+ return "Session ending";
189
+ default:
190
+ return p.message ? String(p.message) : "Event occurred";
191
+ }
192
+ }
package/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @pi-unipi/notify — Extension entry
3
+ *
4
+ * Cross-platform notification system for Pi.
5
+ * Bridges agent lifecycle events to external platforms (native OS, Gotify, Telegram).
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
9
+ import {
10
+ UNIPI_EVENTS,
11
+ MODULES,
12
+ NOTIFY_TOOLS,
13
+ emitEvent,
14
+ getPackageVersion,
15
+ } from "@pi-unipi/core";
16
+ import { registerNotifyTools } from "./tools.js";
17
+ import { registerNotifyCommands } from "./commands.js";
18
+ import { loadConfig } from "./settings.js";
19
+ import {
20
+ registerEventListeners,
21
+ unregisterEventListeners,
22
+ } from "./events.js";
23
+
24
+ /** Package version */
25
+ const VERSION = getPackageVersion(new URL(".", import.meta.url).pathname);
26
+
27
+ export default function (pi: ExtensionAPI) {
28
+ // Register skills directory
29
+ const skillsDir = new URL("./skills", import.meta.url).pathname;
30
+ pi.on("resources_discover", async () => {
31
+ return {
32
+ skillPaths: [skillsDir],
33
+ };
34
+ });
35
+
36
+ // Register tools and commands
37
+ registerNotifyTools(pi);
38
+ registerNotifyCommands(pi);
39
+
40
+ // Session lifecycle — register events and announce module
41
+ pi.on("session_start", async () => {
42
+ const config = loadConfig();
43
+ registerEventListeners(pi, config);
44
+
45
+ emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
46
+ name: MODULES.NOTIFY,
47
+ version: VERSION,
48
+ commands: ["unipi:notify-settings", "unipi:notify-set-gotify", "unipi:notify-set-tg", "unipi:notify-test"],
49
+ tools: [NOTIFY_TOOLS.NOTIFY_USER],
50
+ });
51
+ });
52
+
53
+ // Cleanup on session shutdown
54
+ pi.on("session_shutdown", async () => {
55
+ unregisterEventListeners();
56
+ });
57
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@pi-unipi/notify",
3
+ "version": "0.1.1",
4
+ "description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Neuron Mr White",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Neuron-Mr-White/unipi.git",
11
+ "directory": "packages/notify"
12
+ },
13
+ "keywords": [
14
+ "pi-package",
15
+ "pi-extension",
16
+ "pi-coding-agent",
17
+ "unipi",
18
+ "notify",
19
+ "notifications"
20
+ ],
21
+ "files": [
22
+ "index.ts",
23
+ "tools.ts",
24
+ "commands.ts",
25
+ "settings.ts",
26
+ "events.ts",
27
+ "types.ts",
28
+ "platforms/*",
29
+ "tui/*",
30
+ "skills/**/*",
31
+ "README.md"
32
+ ],
33
+ "pi": {
34
+ "extensions": [
35
+ "index.ts"
36
+ ],
37
+ "skills": [
38
+ "skills"
39
+ ]
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@pi-unipi/core": "*",
46
+ "node-notifier": "^10.0.1"
47
+ },
48
+ "peerDependencies": {
49
+ "@mariozechner/pi-coding-agent": "*",
50
+ "@mariozechner/pi-tui": "*",
51
+ "@sinclair/typebox": "*"
52
+ }
53
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @pi-unipi/notify — Gotify notification platform
3
+ *
4
+ * Sends push notifications to a Gotify server via HTTP POST.
5
+ * Gotify is a self-hosted push notification server.
6
+ */
7
+
8
+ /** Send a notification to Gotify server */
9
+ export async function sendGotifyNotification(
10
+ serverUrl: string,
11
+ appToken: string,
12
+ title: string,
13
+ message: string,
14
+ priority: number = 5
15
+ ): Promise<void> {
16
+ const url = serverUrl.replace(/\/$/, "") + "/message";
17
+ const response = await fetch(url, {
18
+ method: "POST",
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ "X-Gotify-Key": appToken,
22
+ },
23
+ body: JSON.stringify({
24
+ title,
25
+ message,
26
+ priority,
27
+ }),
28
+ });
29
+
30
+ if (!response.ok) {
31
+ const body = await response.text().catch(() => "<no body>");
32
+ throw new Error(
33
+ `Gotify API error ${response.status}: ${body}`
34
+ );
35
+ }
36
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @pi-unipi/notify — Native OS notification platform
3
+ *
4
+ * Wraps node-notifier for cross-platform desktop notifications.
5
+ * Windows: SnoreToast (no admin required)
6
+ * macOS: terminal-notifier
7
+ * Linux: notify-send / libnotify
8
+ */
9
+
10
+ import notifier from "node-notifier";
11
+
12
+ /** Options for native notification */
13
+ export interface NativeNotificationOptions {
14
+ /** Windows appID to show instead of "SnoreToast" */
15
+ windowsAppId?: string;
16
+ }
17
+
18
+ /**
19
+ * Send a native OS notification.
20
+ * Resolves when notification is shown, rejects on error.
21
+ */
22
+ export async function sendNativeNotification(
23
+ title: string,
24
+ message: string,
25
+ options?: NativeNotificationOptions
26
+ ): Promise<void> {
27
+ return new Promise((resolve, reject) => {
28
+ notifier.notify(
29
+ {
30
+ title,
31
+ message,
32
+ appID: options?.windowsAppId,
33
+ },
34
+ (err: Error | null) => {
35
+ if (err) {
36
+ reject(
37
+ new Error(
38
+ `Native notification failed: ${err.message}`
39
+ )
40
+ );
41
+ } else {
42
+ resolve();
43
+ }
44
+ }
45
+ );
46
+ });
47
+ }