@pi-unipi/notify 0.1.9 → 0.1.11

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.
Files changed (3) hide show
  1. package/README.md +51 -74
  2. package/ntfy-config.ts +178 -0
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,21 +1,52 @@
1
1
  # @pi-unipi/notify
2
2
 
3
- Cross-platform notification extension for Pi. Sends push notifications to native OS, Gotify, Telegram, and ntfy when agent lifecycle events occur.
3
+ Push notifications when things happen. Workflow finishes, Ralph loop completes, MCP server errors — notify sends alerts to native OS, Gotify, Telegram, or ntfy.
4
4
 
5
- ## What it does
5
+ Configure once, get alerts everywhere. Per-event platform routing lets you send critical errors to Telegram and routine completions to Gotify.
6
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
7
+ ## Commands
8
+
9
+ | Command | Description |
10
+ |---------|-------------|
11
+ | `/unipi:notify-settings` | Open settings overlay to configure platforms and events |
12
+ | `/unipi:notify-set-gotify` | Configure Gotify server connection |
13
+ | `/unipi:notify-set-tg` | Interactive Telegram bot setup |
14
+ | `/unipi:notify-set-ntfy` | Configure ntfy topic and server |
15
+ | `/unipi:notify-recap-model` | Set model for notification recaps |
16
+ | `/unipi:notify-test` | Send test notification to all enabled platforms |
11
17
 
12
- ## Installation
18
+ ## Special Triggers
13
19
 
14
- Part of the `@pi-unipi/unipi` meta-package. No separate install needed.
20
+ Notify subscribes to Pi lifecycle events and routes notifications based on your config:
21
+
22
+ | Event | Default | Description |
23
+ |-------|---------|-------------|
24
+ | `workflow_end` | On | Workflow command completes |
25
+ | `ralph_loop_end` | On | Ralph loop completes |
26
+ | `mcp_server_error` | On | MCP server error |
27
+ | `agent_end` | Off | Agent finishes responding |
28
+ | `memory_consolidated` | Off | Memory auto-saved |
29
+ | `session_shutdown` | Off | Session ends |
30
+
31
+ Notify registers with the info-screen dashboard, showing enabled platforms and last notification time. The footer subscribes to `NOTIFICATION_SENT` events to display notification stats.
32
+
33
+ ## Agent Tool
34
+
35
+ | Tool | Description |
36
+ |------|-------------|
37
+ | `notify_user` | Send cross-platform notification |
38
+
39
+ ```
40
+ notify_user({
41
+ title: "Build Failed",
42
+ message: "TypeScript compilation failed with 12 errors.",
43
+ priority: "high"
44
+ })
45
+ ```
15
46
 
16
47
  ## Platforms
17
48
 
18
- ### Native OS (default)
49
+ ### Native OS
19
50
 
20
51
  Desktop notifications via [node-notifier](https://github.com/mikaelbr/node-notifier):
21
52
  - **Windows:** SnoreToast (no admin required)
@@ -26,7 +57,7 @@ Zero configuration — works out of the box.
26
57
 
27
58
  ### Gotify
28
59
 
29
- Self-hosted push notification server. Configure in settings:
60
+ Self-hosted push notification server:
30
61
 
31
62
  ```json
32
63
  {
@@ -41,26 +72,14 @@ Self-hosted push notification server. Configure in settings:
41
72
 
42
73
  ### Telegram
43
74
 
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
75
+ Bot API notifications. Run `/unipi:notify-set-tg` for interactive setup:
76
+ 1. Create a bot via @BotFather
77
+ 2. Paste the bot token
78
+ 3. Auto-detect your chat ID
54
79
 
55
80
  ### ntfy
56
81
 
57
- HTTP-based pub-sub notifications via [ntfy.sh](https://ntfy.sh) or self-hosted. Run setup command:
58
-
59
- ```
60
- /unipi:notify-set-ntfy
61
- ```
62
-
63
- Or configure manually:
82
+ HTTP-based pub-sub notifications via [ntfy.sh](https://ntfy.sh) or self-hosted:
64
83
 
65
84
  ```json
66
85
  {
@@ -73,54 +92,12 @@ Or configure manually:
73
92
  }
74
93
  ```
75
94
 
76
- ## Commands
77
-
78
- | Command | Description |
79
- |---------|-------------|
80
- | `/unipi:notify-settings` | Open settings overlay to configure platforms and events |
81
- | `/unipi:notify-set-gotify` | Configure Gotify server connection |
82
- | `/unipi:notify-set-tg` | Interactive Telegram bot setup |
83
- | `/unipi:notify-set-ntfy` | Configure ntfy topic and server |
84
- | `/unipi:notify-test` | Send test notification to all enabled platforms |
85
-
86
- ## Agent Tool
87
-
88
- The `notify_user` tool is available to the agent for ad-hoc notifications:
89
-
90
- ```
91
- notify_user({
92
- title: "Build Failed",
93
- message: "TypeScript compilation failed with 12 errors.",
94
- priority: "high"
95
- })
96
- ```
97
-
98
- See the bundled `notify` skill for full parameter documentation.
99
-
100
- ## Event Configuration
101
-
102
- Notifications are triggered by these events (configurable in settings):
103
-
104
- | Event | Default | Description |
105
- |-------|---------|-------------|
106
- | `workflow_end` | On | Workflow command completes |
107
- | `ralph_loop_end` | On | Ralph loop completes |
108
- | `mcp_server_error` | On | MCP server error |
109
- | `agent_end` | Off | Agent finishes responding |
110
- | `memory_consolidated` | Off | Memory auto-saved |
111
- | `session_shutdown` | Off | Session ends |
95
+ ## Configurables
112
96
 
113
- ## Configuration
97
+ Settings stored at `~/.unipi/config/notify/config.json`. Edit via `/unipi:notify-settings` or manual JSON editing.
114
98
 
115
- Settings stored at `~/.unipi/config/notify/config.json`. Edit via:
116
- - Settings overlay: `/unipi:notify-settings`
117
- - Manual JSON editing
118
- - The agent can read config via the settings module
99
+ Per-event platform routing lets you control where each event type goes. The settings overlay shows all events with platform toggles.
119
100
 
120
- ## Info-Screen Integration
101
+ ## License
121
102
 
122
- The notify module registers with the info screen showing:
123
- - Enabled platform count
124
- - Active event subscriptions
125
- - Last notification timestamp
126
- - Total notifications sent this session
103
+ MIT
package/ntfy-config.ts ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @pi-unipi/notify — Project-level ntfy configuration
3
+ *
4
+ * Loads, saves, and resolves ntfy config from dedicated ntfy.json files
5
+ * at global (~/.unipi/config/notify/ntfy.json) and project
6
+ * (<cwd>/.unipi/config/notify/ntfy.json) scope.
7
+ *
8
+ * Resolution: project → global → defaults.
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
12
+ import { dirname, join } from "path";
13
+ import { homedir } from "os";
14
+ import type { NtfyConfig } from "./types.js";
15
+
16
+ /** Default ntfy configuration */
17
+ const DEFAULT_NTFY_CONFIG: NtfyConfig = {
18
+ enabled: false,
19
+ serverUrl: "https://ntfy.sh",
20
+ priority: 3,
21
+ };
22
+
23
+ /** Global ntfy.json path: ~/.unipi/config/notify/ntfy.json */
24
+ function getGlobalNtfyPath(): string {
25
+ return join(homedir(), ".unipi", "config", "notify", "ntfy.json");
26
+ }
27
+
28
+ /** Project ntfy.json path: <cwd>/.unipi/config/notify/ntfy.json */
29
+ function getProjectNtfyPath(cwd: string): string {
30
+ return join(cwd, ".unipi", "config", "notify", "ntfy.json");
31
+ }
32
+
33
+ /**
34
+ * Read and parse a ntfy.json file.
35
+ * Returns null if file doesn't exist (ENOENT).
36
+ * Returns null and logs warning on parse error.
37
+ */
38
+ function readNtfyJson(filePath: string): NtfyConfig | null {
39
+ try {
40
+ const raw = readFileSync(filePath, "utf-8");
41
+ const parsed = JSON.parse(raw) as Partial<NtfyConfig>;
42
+ return { ...DEFAULT_NTFY_CONFIG, ...parsed };
43
+ } catch (err: unknown) {
44
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
45
+ return null;
46
+ }
47
+ console.warn(`[notify] Failed to parse ${filePath}: ${(err as Error).message}. Falling back.`);
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Resolve ntfy config with project → global → defaults priority.
54
+ *
55
+ * 1. If project ntfy.json exists → use it
56
+ * 2. If only global ntfy.json exists → use it
57
+ * 3. If neither exists → return defaults (ntfy disabled)
58
+ *
59
+ * Runs legacy migration once if global ntfy.json is missing.
60
+ */
61
+ export function loadNtfyConfig(cwd: string): NtfyConfig {
62
+ // Attempt migration from legacy config.json if global ntfy.json doesn't exist
63
+ migrateFromLegacyConfig();
64
+
65
+ // Try project-level first
66
+ const projectConfig = readNtfyJson(getProjectNtfyPath(cwd));
67
+ if (projectConfig !== null) {
68
+ return projectConfig;
69
+ }
70
+
71
+ // Try global level
72
+ const globalConfig = readNtfyJson(getGlobalNtfyPath());
73
+ if (globalConfig !== null) {
74
+ return globalConfig;
75
+ }
76
+
77
+ // Neither exists — return defaults
78
+ return { ...DEFAULT_NTFY_CONFIG };
79
+ }
80
+
81
+ /**
82
+ * Save ntfy config to the chosen scope.
83
+ * Creates parent directory if needed.
84
+ */
85
+ export function saveNtfyConfig(
86
+ scope: "project" | "global",
87
+ cwd: string,
88
+ config: NtfyConfig
89
+ ): void {
90
+ const filePath =
91
+ scope === "project" ? getProjectNtfyPath(cwd) : getGlobalNtfyPath();
92
+ const dir = dirname(filePath);
93
+ mkdirSync(dir, { recursive: true });
94
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
95
+ }
96
+
97
+ /**
98
+ * Detect which scope is currently active for ntfy config.
99
+ *
100
+ * - "project" if project ntfy.json exists
101
+ * - "global" if global ntfy.json exists (and no project override)
102
+ * - "none" if neither exists
103
+ */
104
+ export function getNtfyConfigScope(
105
+ cwd: string
106
+ ): "project" | "global" | "none" {
107
+ if (existsSync(getProjectNtfyPath(cwd))) {
108
+ return "project";
109
+ }
110
+ if (existsSync(getGlobalNtfyPath())) {
111
+ return "global";
112
+ }
113
+ return "none";
114
+ }
115
+
116
+ /**
117
+ * One-time migration from legacy config.json ntfy section to ntfy.json.
118
+ *
119
+ * Trigger conditions:
120
+ * - Global ntfy.json does NOT exist
121
+ * - config.json has non-default ntfy settings (enabled or topic/serverUrl set)
122
+ *
123
+ * After migration, config.json ntfy section is left untouched.
124
+ * Future reads use ntfy.json exclusively.
125
+ */
126
+ export function migrateFromLegacyConfig(): void {
127
+ const globalNtfyPath = getGlobalNtfyPath();
128
+
129
+ // Only migrate if global ntfy.json doesn't exist yet
130
+ if (existsSync(globalNtfyPath)) {
131
+ return;
132
+ }
133
+
134
+ // Try to read legacy config.json
135
+ const legacyConfigPath = join(
136
+ homedir(),
137
+ ".unipi",
138
+ "config",
139
+ "notify",
140
+ "config.json"
141
+ );
142
+
143
+ try {
144
+ if (!existsSync(legacyConfigPath)) return;
145
+ const raw = readFileSync(legacyConfigPath, "utf-8");
146
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
147
+ const ntfySection = parsed.ntfy as Partial<NtfyConfig> | undefined;
148
+
149
+ if (!ntfySection) return;
150
+
151
+ // Only migrate if there's something meaningful to migrate
152
+ const hasCustomConfig =
153
+ ntfySection.enabled === true ||
154
+ (ntfySection.serverUrl && ntfySection.serverUrl !== "https://ntfy.sh") ||
155
+ ntfySection.topic;
156
+
157
+ if (!hasCustomConfig) return;
158
+
159
+ // Write ntfy.json from legacy config
160
+ const migratedConfig: NtfyConfig = {
161
+ enabled: ntfySection.enabled ?? false,
162
+ serverUrl: ntfySection.serverUrl ?? "https://ntfy.sh",
163
+ topic: ntfySection.topic,
164
+ token: ntfySection.token,
165
+ priority: ntfySection.priority ?? 3,
166
+ };
167
+
168
+ const dir = dirname(globalNtfyPath);
169
+ mkdirSync(dir, { recursive: true });
170
+ writeFileSync(
171
+ globalNtfyPath,
172
+ JSON.stringify(migratedConfig, null, 2) + "\n",
173
+ "utf-8"
174
+ );
175
+ } catch {
176
+ // Migration failure is non-fatal — silently continue
177
+ }
178
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/notify",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Cross-platform notification extension for Pi — native OS, Gotify, and Telegram notifications for agent lifecycle events",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,6 +24,7 @@
24
24
  "commands.ts",
25
25
  "settings.ts",
26
26
  "events.ts",
27
+ "ntfy-config.ts",
27
28
  "summarize.ts",
28
29
  "types.ts",
29
30
  "platforms/*",