@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 +103 -0
- package/commands.ts +210 -0
- package/events.ts +192 -0
- package/index.ts +57 -0
- package/package.json +53 -0
- package/platforms/gotify.ts +36 -0
- package/platforms/native.ts +47 -0
- package/platforms/node-notifier.d.ts +20 -0
- package/platforms/telegram.ts +77 -0
- package/settings.ts +115 -0
- package/skills/configure-notify/SKILL.md +134 -0
- package/skills/notify/SKILL.md +56 -0
- package/tools.ts +96 -0
- package/tui/gotify-setup.ts +528 -0
- package/tui/settings-overlay.ts +251 -0
- package/tui/telegram-setup.ts +301 -0
- package/types.ts +88 -0
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
|
+
}
|