@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.
@@ -0,0 +1,20 @@
1
+ declare module "node-notifier" {
2
+ interface Notification {
3
+ title?: string;
4
+ message?: string;
5
+ appID?: string;
6
+ sound?: boolean | string;
7
+ icon?: string;
8
+ wait?: boolean;
9
+ }
10
+
11
+ interface Notifier {
12
+ notify(
13
+ notification: Notification,
14
+ callback: (err: Error | null, data: any) => void
15
+ ): void;
16
+ }
17
+
18
+ const notifier: Notifier;
19
+ export default notifier;
20
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @pi-unipi/notify — Telegram notification platform
3
+ *
4
+ * Sends notifications via Telegram Bot API.
5
+ * Supports auto-detection of chat ID via polling getUpdates.
6
+ */
7
+
8
+ /** Send a notification via Telegram Bot API */
9
+ export async function sendTelegramNotification(
10
+ botToken: string,
11
+ chatId: string,
12
+ title: string,
13
+ message: string
14
+ ): Promise<void> {
15
+ const text = `*${escapeMarkdown(title)}*\n${escapeMarkdown(message)}`;
16
+ const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
17
+
18
+ const response = await fetch(url, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify({
22
+ chat_id: chatId,
23
+ text,
24
+ parse_mode: "MarkdownV2",
25
+ }),
26
+ });
27
+
28
+ if (!response.ok) {
29
+ const body = await response.text().catch(() => "<no body>");
30
+ throw new Error(
31
+ `Telegram API error ${response.status}: ${body}`
32
+ );
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Poll Telegram getUpdates to detect the chat ID from a user message.
38
+ * Returns the chat ID string, or null if no message found.
39
+ */
40
+ export async function pollForChatId(
41
+ botToken: string,
42
+ signal?: AbortSignal
43
+ ): Promise<string | null> {
44
+ const url = `https://api.telegram.org/bot${botToken}/getUpdates?allowed_updates=["message"]`;
45
+
46
+ try {
47
+ const response = await fetch(url, { signal });
48
+ const data = (await response.json()) as {
49
+ ok: boolean;
50
+ result: Array<{
51
+ message?: { chat?: { id?: number } };
52
+ callback_query?: { message?: { chat?: { id?: number } } };
53
+ }>;
54
+ };
55
+
56
+ if (data.ok && data.result.length > 0) {
57
+ const lastUpdate = data.result[data.result.length - 1];
58
+ const chatId =
59
+ lastUpdate.message?.chat?.id ||
60
+ lastUpdate.callback_query?.message?.chat?.id;
61
+ if (chatId) {
62
+ return String(chatId);
63
+ }
64
+ }
65
+ } catch (err) {
66
+ if (signal?.aborted) throw err;
67
+ // Network error — return null to allow retry
68
+ return null;
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ /** Escape special MarkdownV2 characters */
75
+ function escapeMarkdown(text: string): string {
76
+ return text.replace(/[_*\[\]()~`>#+\-=|{}.!\\]/g, "\\$&");
77
+ }
package/settings.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @pi-unipi/notify — Configuration management
3
+ *
4
+ * Loads, saves, and validates notification config from ~/.unipi/config/notify/config.json
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
8
+ import { dirname, join } from "path";
9
+ import { homedir } from "os";
10
+ import { NOTIFY_DIRS } from "@pi-unipi/core";
11
+ import type { NotifyConfig } from "./types.js";
12
+
13
+ /** Resolve config path (expands ~ to homedir) */
14
+ function resolveConfigPath(): string {
15
+ const base = NOTIFY_DIRS.CONFIG.replace("~", homedir());
16
+ return join(base, "config.json");
17
+ }
18
+
19
+ /** Default configuration — native enabled, gotify/telegram disabled */
20
+ export const DEFAULT_CONFIG: NotifyConfig = {
21
+ defaultPlatforms: ["native"],
22
+ events: {
23
+ workflow_end: { enabled: true, platforms: [] },
24
+ ralph_loop_end: { enabled: true, platforms: [] },
25
+ mcp_server_error: { enabled: true, platforms: [] },
26
+ agent_end: { enabled: false, platforms: [] },
27
+ memory_consolidated: { enabled: false, platforms: [] },
28
+ session_shutdown: { enabled: false, platforms: [] },
29
+ },
30
+ native: {
31
+ enabled: true,
32
+ },
33
+ gotify: {
34
+ enabled: false,
35
+ priority: 5,
36
+ },
37
+ telegram: {
38
+ enabled: false,
39
+ },
40
+ };
41
+
42
+ /** Load config from disk, returning defaults if missing or invalid */
43
+ export function loadConfig(): NotifyConfig {
44
+ const configPath = resolveConfigPath();
45
+ try {
46
+ if (existsSync(configPath)) {
47
+ const raw = readFileSync(configPath, "utf-8");
48
+ const parsed = JSON.parse(raw) as Partial<NotifyConfig>;
49
+ // Merge with defaults to ensure new fields are present
50
+ return mergeWithDefaults(parsed);
51
+ }
52
+ } catch (err) {
53
+ console.warn(
54
+ `[notify] Failed to load config from ${configPath}, using defaults:`,
55
+ err
56
+ );
57
+ }
58
+ return { ...DEFAULT_CONFIG };
59
+ }
60
+
61
+ /** Save config to disk, creating directory if needed */
62
+ export function saveConfig(config: NotifyConfig): void {
63
+ const configPath = resolveConfigPath();
64
+ const dir = dirname(configPath);
65
+ mkdirSync(dir, { recursive: true });
66
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
67
+ }
68
+
69
+ /** Update config with partial changes */
70
+ export function updateConfig(partial: Partial<NotifyConfig>): NotifyConfig {
71
+ const current = loadConfig();
72
+ const updated = { ...current, ...partial };
73
+ saveConfig(updated);
74
+ return updated;
75
+ }
76
+
77
+ /** Validate that a config has required fields for enabled platforms */
78
+ export function validateConfig(config: NotifyConfig): string[] {
79
+ const errors: string[] = [];
80
+
81
+ if (config.gotify.enabled) {
82
+ if (!config.gotify.serverUrl) {
83
+ errors.push("Gotify: serverUrl is required");
84
+ }
85
+ if (!config.gotify.appToken) {
86
+ errors.push("Gotify: appToken is required");
87
+ }
88
+ }
89
+
90
+ if (config.telegram.enabled) {
91
+ if (!config.telegram.botToken) {
92
+ errors.push("Telegram: botToken is required");
93
+ }
94
+ if (!config.telegram.chatId) {
95
+ errors.push("Telegram: chatId is required");
96
+ }
97
+ }
98
+
99
+ if (config.gotify.priority < 1 || config.gotify.priority > 10) {
100
+ errors.push("Gotify: priority must be between 1 and 10");
101
+ }
102
+
103
+ return errors;
104
+ }
105
+
106
+ /** Merge loaded config with defaults to ensure all fields exist */
107
+ function mergeWithDefaults(loaded: Partial<NotifyConfig>): NotifyConfig {
108
+ return {
109
+ defaultPlatforms: loaded.defaultPlatforms ?? DEFAULT_CONFIG.defaultPlatforms,
110
+ events: { ...DEFAULT_CONFIG.events, ...loaded.events },
111
+ native: { ...DEFAULT_CONFIG.native, ...loaded.native },
112
+ gotify: { ...DEFAULT_CONFIG.gotify, ...loaded.gotify },
113
+ telegram: { ...DEFAULT_CONFIG.telegram, ...loaded.telegram },
114
+ };
115
+ }
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: configure-notify
3
+ description: >
4
+ Help user configure Pi notification settings — platforms (native, Gotify, Telegram),
5
+ events, and per-event routing. Guide through setup or make changes directly.
6
+ ---
7
+
8
+ # Configure Notify
9
+
10
+ Help users configure the `@pi-unipi/notify` notification system.
11
+
12
+ ## When to use
13
+
14
+ - User asks to set up notifications
15
+ - User asks to enable/configure Gotify, Telegram, or native notifications
16
+ - User wants to change which events trigger notifications
17
+ - User asks about notification settings
18
+
19
+ ## Config location
20
+
21
+ `~/.unipi/config/notify/config.json`
22
+
23
+ ## Config structure
24
+
25
+ ```json
26
+ {
27
+ "defaultPlatforms": ["native"],
28
+ "events": {
29
+ "workflow_end": { "enabled": true, "platforms": [] },
30
+ "ralph_loop_end": { "enabled": true, "platforms": [] },
31
+ "mcp_server_error": { "enabled": true, "platforms": [] },
32
+ "agent_end": { "enabled": false, "platforms": [] },
33
+ "memory_consolidated": { "enabled": false, "platforms": [] },
34
+ "session_shutdown": { "enabled": false, "platforms": [] }
35
+ },
36
+ "native": {
37
+ "enabled": true,
38
+ "windowsAppId": null
39
+ },
40
+ "gotify": {
41
+ "enabled": false,
42
+ "serverUrl": null,
43
+ "appToken": null,
44
+ "priority": 5
45
+ },
46
+ "telegram": {
47
+ "enabled": false,
48
+ "botToken": null,
49
+ "chatId": null
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Platforms
55
+
56
+ ### Native OS (default: enabled)
57
+
58
+ Desktop notifications via node-notifier. Works out of the box on Windows, macOS, Linux.
59
+
60
+ ### Gotify (default: disabled)
61
+
62
+ Self-hosted push notification server. Requires:
63
+ - `serverUrl` — URL of your Gotify server (e.g. `https://gotify.example.com`)
64
+ - `appToken` — Application token from Gotify web UI (Apps → Create Application)
65
+ - `priority` — 1-10 (default: 5)
66
+
67
+ **Setup options:**
68
+ 1. **Interactive overlay:** Tell user to run `/unipi:notify-set-gotify` for guided setup with connection test
69
+ 2. **Manual config:** Edit `config.json` directly with the fields above
70
+ 3. **Agent can write config:** Read the current config, merge changes, write back
71
+
72
+ ### Telegram (default: disabled)
73
+
74
+ Bot API notifications. Requires:
75
+ - `botToken` — From @BotFather
76
+ - `chatId` — Auto-detected by `/unipi:notify-set-tg`
77
+
78
+ ## Commands
79
+
80
+ | Command | Description |
81
+ |---------|-------------|
82
+ | `/unipi:notify-settings` | TUI overlay to toggle platforms and events |
83
+ | `/unipi:notify-set-gotify` | Interactive Gotify setup wizard |
84
+ | `/unipi:notify-set-tg` | Interactive Telegram setup wizard |
85
+ | `/unipi:notify-test` | Send test notification to all enabled platforms |
86
+
87
+ ## Events
88
+
89
+ | Event | Default | Description |
90
+ |-------|---------|-------------|
91
+ | `workflow_end` | On | Workflow command completes |
92
+ | `ralph_loop_end` | On | Ralph loop completes |
93
+ | `mcp_server_error` | On | MCP server error |
94
+ | `agent_end` | Off | Agent finishes responding |
95
+ | `memory_consolidated` | Off | Memory auto-saved |
96
+ | `session_shutdown` | Off | Session ends |
97
+
98
+ Each event can override `platforms` — empty array means use `defaultPlatforms`.
99
+
100
+ ## Agent workflow
101
+
102
+ ### Reading current config
103
+
104
+ ```bash
105
+ cat ~/.unipi/config/notify/config.json
106
+ ```
107
+
108
+ ### Updating config programmatically
109
+
110
+ Read the JSON, make changes, write it back. Example:
111
+
112
+ ```json
113
+ // To enable Gotify:
114
+ {
115
+ "gotify": {
116
+ "enabled": true,
117
+ "serverUrl": "https://gotify.example.com",
118
+ "appToken": "AT_xxxxx",
119
+ "priority": 7
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### Guiding user to interactive setup
125
+
126
+ For Gotify: suggest running `/unipi:notify-set-gotify`
127
+ For Telegram: suggest running `/unipi:notify-set-tg`
128
+ For general settings: suggest `/unipi:notify-settings`
129
+
130
+ ## Validation rules
131
+
132
+ - Gotify: `serverUrl` and `appToken` required when enabled
133
+ - Gotify: `priority` must be 1-10
134
+ - Telegram: `botToken` and `chatId` required when enabled
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: notify
3
+ description: >
4
+ Cross-platform notification system for Pi. Use notify_user when you need
5
+ to urgently alert the user about critical findings, errors, or completion
6
+ of long-running tasks.
7
+ allowed-tools:
8
+ - notify_user
9
+ ---
10
+
11
+ # Notify User
12
+
13
+ Use the `notify_user` tool to send notifications to the user's configured
14
+ platforms (native OS, Gotify, Telegram).
15
+
16
+ ## When to use notify_user
17
+
18
+ - Critical errors that need immediate attention
19
+ - Completion of long-running tasks (after user has been waiting)
20
+ - Security concerns or suspicious activity detected
21
+ - Results that the user explicitly asked to be notified about
22
+
23
+ ## When NOT to use notify_user
24
+
25
+ - Routine status updates (use normal message instead)
26
+ - Non-urgent information (let user read at their pace)
27
+ - Every turn completion (spammy)
28
+
29
+ ## Parameters
30
+
31
+ | Parameter | Type | Default | Description |
32
+ |-----------|------|---------|-------------|
33
+ | `message` | string | required | Notification body |
34
+ | `title` | string? | "Pi Notification" | Notification title |
35
+ | `priority` | string? | "normal" | "low", "normal", or "high" |
36
+ | `platforms` | string[]? | all enabled | Override which platforms to use |
37
+
38
+ ## Examples
39
+
40
+ Alert on critical error:
41
+ ```
42
+ notify_user({
43
+ title: "Build Failed",
44
+ message: "TypeScript compilation failed with 12 errors. Check src/auth.ts.",
45
+ priority: "high"
46
+ })
47
+ ```
48
+
49
+ Task completion:
50
+ ```
51
+ notify_user({
52
+ title: "Deployment Complete",
53
+ message: "Successfully deployed to production. All health checks passed.",
54
+ priority: "normal"
55
+ })
56
+ ```
package/tools.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @pi-unipi/notify — Agent tool registration
3
+ *
4
+ * Registers the `notify_user` tool for ad-hoc notifications.
5
+ */
6
+
7
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
8
+ import { Type } from "@sinclair/typebox";
9
+ import { NOTIFY_TOOLS } from "@pi-unipi/core";
10
+ import { loadConfig } from "./settings.js";
11
+ import { dispatchNotification } from "./events.js";
12
+ import type { NotifyDispatchResult } from "./types.js";
13
+
14
+ /** Schema for notify_user tool parameters */
15
+ const NotifyUserSchema = Type.Object({
16
+ message: Type.String({ description: "Notification message body" }),
17
+ title: Type.Optional(
18
+ Type.String({ description: "Notification title (default: Pi Notification)" })
19
+ ),
20
+ priority: Type.Optional(
21
+ Type.String({
22
+ enum: ["low", "normal", "high"],
23
+ default: "normal",
24
+ description: "Priority level",
25
+ })
26
+ ),
27
+ platforms: Type.Optional(
28
+ Type.Array(
29
+ Type.String({ enum: ["native", "gotify", "telegram"] }),
30
+ { description: "Override platforms for this notification" }
31
+ )
32
+ ),
33
+ });
34
+
35
+ /**
36
+ * Register the notify_user tool with pi.
37
+ */
38
+ export function registerNotifyTools(pi: ExtensionAPI): void {
39
+ pi.registerTool({
40
+ name: NOTIFY_TOOLS.NOTIFY_USER,
41
+ label: "Notify User",
42
+ description:
43
+ "Send a notification to the user's configured platforms (native OS, Gotify, Telegram). " +
44
+ "Use for critical errors, completion of long-running tasks, or when the user explicitly asked to be notified.",
45
+ parameters: NotifyUserSchema,
46
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx: ExtensionContext) {
47
+ const {
48
+ message,
49
+ title,
50
+ priority: _priority,
51
+ platforms,
52
+ } = params as {
53
+ message: string;
54
+ title?: string;
55
+ priority?: "low" | "normal" | "high";
56
+ platforms?: Array<"native" | "gotify" | "telegram">;
57
+ };
58
+
59
+ const config = loadConfig();
60
+
61
+ // Resolve title
62
+ const notifTitle = title || "Pi Notification";
63
+
64
+ // Resolve platforms — use params.platforms or global defaults
65
+ const notifPlatforms = platforms || config.defaultPlatforms;
66
+
67
+ // Dispatch notification
68
+ const result: NotifyDispatchResult = await dispatchNotification(
69
+ pi,
70
+ notifTitle,
71
+ message,
72
+ notifPlatforms,
73
+ "agent_tool",
74
+ config
75
+ );
76
+
77
+ // Format result
78
+ const platformResults = result.results.map(
79
+ (r) => `${r.platform}: ${r.success ? "✓ sent" : `✗ ${r.error || "failed"}`}`
80
+ );
81
+
82
+ return {
83
+ content: [
84
+ {
85
+ type: "text" as const,
86
+ text: `Notification sent to ${result.results.length} platform(s):\n${platformResults.join("\n")}`,
87
+ },
88
+ ],
89
+ details: {
90
+ platforms: result.results.map((r) => r.platform),
91
+ allSuccess: result.allSuccess,
92
+ },
93
+ };
94
+ },
95
+ });
96
+ }