@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,251 @@
1
+ /**
2
+ * @pi-unipi/notify — Settings TUI Component
3
+ *
4
+ * Interactive settings editor for notification configuration.
5
+ * Allows toggling platforms, configuring credentials, and per-event settings.
6
+ */
7
+
8
+ import type { Component } from "@mariozechner/pi-tui";
9
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
10
+ import type { Theme } from "@mariozechner/pi-coding-agent";
11
+ import {
12
+ loadConfig,
13
+ saveConfig,
14
+ validateConfig,
15
+ } from "../settings.js";
16
+ import type { NotifyConfig } from "../types.js";
17
+
18
+ /** Section types */
19
+ type Section = "platforms" | "events";
20
+
21
+ /**
22
+ * Settings overlay component.
23
+ */
24
+ export class NotifySettingsOverlay implements Component {
25
+ private config: NotifyConfig;
26
+ private section: Section = "platforms";
27
+ private selectedIndex = 0;
28
+ private error: string | null = null;
29
+ private saved = false;
30
+ onClose?: () => void;
31
+ requestRender?: () => void;
32
+ private theme: Theme | null = null;
33
+
34
+ constructor() {
35
+ this.config = loadConfig();
36
+ }
37
+
38
+ setTheme(theme: Theme): void {
39
+ this.theme = theme;
40
+ }
41
+
42
+ invalidate(): void {}
43
+
44
+ handleInput(data: string): void {
45
+ switch (data) {
46
+ case "\x1b[A": // Up
47
+ case "k":
48
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
49
+ break;
50
+ case "\x1b[B": // Down
51
+ case "j":
52
+ this.selectedIndex = Math.min(this.maxItems - 1, this.selectedIndex + 1);
53
+ break;
54
+ case " ": // Space - toggle
55
+ this.toggleCurrent();
56
+ break;
57
+ case "\t": // Tab - switch section
58
+ this.section = this.section === "platforms" ? "events" : "platforms";
59
+ this.selectedIndex = 0;
60
+ break;
61
+ case "\r": // Enter - save
62
+ this.save();
63
+ break;
64
+ case "\x1b": // Escape - close
65
+ this.onClose?.();
66
+ break;
67
+ }
68
+ }
69
+
70
+ private get maxItems(): number {
71
+ if (this.section === "platforms") return 3; // native, gotify, telegram
72
+ return Object.keys(this.config.events).length;
73
+ }
74
+
75
+ private toggleCurrent(): void {
76
+ if (this.section === "platforms") {
77
+ const platforms: Array<"native" | "gotify" | "telegram"> = [
78
+ "native",
79
+ "gotify",
80
+ "telegram",
81
+ ];
82
+ const key = platforms[this.selectedIndex];
83
+ if (key) {
84
+ this.config[key].enabled = !this.config[key].enabled;
85
+ }
86
+ } else {
87
+ const eventKeys = Object.keys(this.config.events);
88
+ const key = eventKeys[this.selectedIndex];
89
+ if (key && this.config.events[key]) {
90
+ this.config.events[key].enabled = !this.config.events[key].enabled;
91
+ }
92
+ }
93
+ }
94
+
95
+ private save(): void {
96
+ const errors = validateConfig(this.config);
97
+ if (errors.length > 0) {
98
+ this.error = errors.join("; ");
99
+ return;
100
+ }
101
+ this.error = null;
102
+ saveConfig(this.config);
103
+ this.saved = true;
104
+ setTimeout(() => this.onClose?.(), 500);
105
+ }
106
+
107
+ // ─── Theme helpers ───────────────────────────────────────────────────
108
+
109
+ private fg(color: string, text: string): string {
110
+ if (this.theme) return this.theme.fg(color as any, text);
111
+ const c: Record<string, string> = {
112
+ accent: "\x1b[36m", success: "\x1b[32m", warning: "\x1b[33m",
113
+ error: "\x1b[31m", dim: "\x1b[2m", borderMuted: "\x1b[90m",
114
+ };
115
+ return `${c[color] ?? ""}${text}\x1b[0m`;
116
+ }
117
+
118
+ private bold(text: string): string {
119
+ return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
120
+ }
121
+
122
+ private frameLine(content: string, innerWidth: number): string {
123
+ const truncated = truncateToWidth(content, innerWidth, "");
124
+ const padding = Math.max(0, innerWidth - visibleWidth(truncated));
125
+ return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
126
+ }
127
+
128
+ private ruleLine(innerWidth: number): string {
129
+ return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
130
+ }
131
+
132
+ private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
133
+ const left = edge === "top" ? "┌" : "└";
134
+ const right = edge === "top" ? "┐" : "┘";
135
+ return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
136
+ }
137
+
138
+ private getDialogHeight(): number {
139
+ const terminalRows = process.stdout.rows ?? 30;
140
+ return Math.max(14, Math.min(24, Math.floor(terminalRows * 0.65)));
141
+ }
142
+
143
+ render(width: number): string[] {
144
+ const innerWidth = Math.max(22, width - 2);
145
+ const lines: string[] = [];
146
+
147
+ lines.push(this.borderLine(innerWidth, "top"));
148
+ lines.push(this.frameLine(this.fg("accent", this.bold("🔔 Notify Settings")), innerWidth));
149
+ lines.push(this.frameLine(this.fg("dim", "Configure notification platforms and events"), innerWidth));
150
+ lines.push(this.ruleLine(innerWidth));
151
+
152
+ // Section tabs
153
+ const platformTab =
154
+ this.section === "platforms"
155
+ ? this.fg("accent", this.bold("[Platforms]"))
156
+ : this.fg("dim", "Platforms");
157
+ const eventsTab =
158
+ this.section === "events"
159
+ ? this.fg("accent", this.bold("[Events]"))
160
+ : this.fg("dim", "Events");
161
+ lines.push(this.frameLine(` ${platformTab} ${eventsTab}`, innerWidth));
162
+ lines.push(this.ruleLine(innerWidth));
163
+
164
+ if (this.section === "platforms") {
165
+ this.renderPlatforms(lines, innerWidth);
166
+ } else {
167
+ this.renderEvents(lines, innerWidth);
168
+ }
169
+
170
+ // Status messages
171
+ if (this.error) {
172
+ lines.push(this.ruleLine(innerWidth));
173
+ lines.push(this.frameLine(` ${this.fg("error", `⚠ ${this.error}`)}`, innerWidth));
174
+ }
175
+ if (this.saved) {
176
+ lines.push(this.ruleLine(innerWidth));
177
+ lines.push(this.frameLine(` ${this.fg("success", "✓ Settings saved")}`, innerWidth));
178
+ }
179
+
180
+ // Footer
181
+ lines.push(this.ruleLine(innerWidth));
182
+ lines.push(this.frameLine(this.fg("dim", "↑↓ navigate · Space toggle · Tab switch · Enter save · Esc cancel"), innerWidth));
183
+ lines.push(this.borderLine(innerWidth, "bottom"));
184
+
185
+ return lines;
186
+ }
187
+
188
+ private renderPlatforms(lines: string[], innerWidth: number): void {
189
+ const platforms: Array<{
190
+ key: "native" | "gotify" | "telegram";
191
+ label: string;
192
+ detail: string;
193
+ }> = [
194
+ {
195
+ key: "native",
196
+ label: "Native OS",
197
+ detail: "Desktop notifications (node-notifier)",
198
+ },
199
+ {
200
+ key: "gotify",
201
+ label: "Gotify",
202
+ detail: this.config.gotify.serverUrl
203
+ ? `Server: ${this.config.gotify.serverUrl}`
204
+ : "Self-hosted push server",
205
+ },
206
+ {
207
+ key: "telegram",
208
+ label: "Telegram",
209
+ detail: this.config.telegram.botToken
210
+ ? "Bot configured"
211
+ : "Bot API notifications",
212
+ },
213
+ ];
214
+
215
+ for (let i = 0; i < platforms.length; i++) {
216
+ const p = platforms[i];
217
+ const isSelected = i === this.selectedIndex;
218
+ const toggleOn = this.fg("success", "●");
219
+ const toggleOff = this.fg("dim", "○");
220
+ const toggle = this.config[p.key].enabled ? toggleOn : toggleOff;
221
+ const label = isSelected ? this.bold(p.label) : this.fg("dim", p.label);
222
+
223
+ lines.push(
224
+ this.frameLine(
225
+ `${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label} ${this.fg("dim", p.detail)}`,
226
+ innerWidth
227
+ )
228
+ );
229
+ }
230
+ }
231
+
232
+ private renderEvents(lines: string[], innerWidth: number): void {
233
+ const events = Object.entries(this.config.events);
234
+
235
+ for (let i = 0; i < events.length; i++) {
236
+ const [key, cfg] = events[i];
237
+ const isSelected = i === this.selectedIndex;
238
+ const toggleOn = this.fg("success", "●");
239
+ const toggleOff = this.fg("dim", "○");
240
+ const toggle = cfg.enabled ? toggleOn : toggleOff;
241
+ const label = isSelected ? this.bold(key) : this.fg("dim", key);
242
+
243
+ lines.push(
244
+ this.frameLine(
245
+ `${isSelected ? this.fg("accent", "▸") : " "} ${toggle} ${label}`,
246
+ innerWidth
247
+ )
248
+ );
249
+ }
250
+ }
251
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * @pi-unipi/notify — Telegram Setup TUI Component
3
+ *
4
+ * Interactive overlay for setting up Telegram bot notifications.
5
+ * Guides user through BotFather flow and auto-detects chat ID.
6
+ */
7
+
8
+ import type { Component } from "@mariozechner/pi-tui";
9
+ import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
10
+ import type { Theme } from "@mariozechner/pi-coding-agent";
11
+ import { pollForChatId } from "../platforms/telegram.js";
12
+ import { updateConfig } from "../settings.js";
13
+
14
+ type SetupPhase = "instructions" | "token" | "polling" | "success" | "error" | "timeout";
15
+
16
+ /** Spinner frames */
17
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
18
+
19
+ /**
20
+ * Telegram setup overlay component.
21
+ */
22
+ export class TelegramSetupOverlay implements Component {
23
+ private phase: SetupPhase = "instructions";
24
+ private botToken = "";
25
+ private chatId: string | null = null;
26
+ private error: string | null = null;
27
+ private spinnerFrame = 0;
28
+ private spinnerTimer: ReturnType<typeof setInterval> | null = null;
29
+ private pollAbort: AbortController | null = null;
30
+ private pollTimer: ReturnType<typeof setTimeout> | null = null;
31
+ private startTime = Date.now();
32
+ private readonly TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
33
+ private isInPaste = false;
34
+ private pasteBuffer = "";
35
+ onClose?: () => void;
36
+ requestRender?: () => void;
37
+ private theme: Theme | null = null;
38
+
39
+ setTheme(theme: Theme): void {
40
+ this.theme = theme;
41
+ }
42
+
43
+ invalidate(): void {}
44
+
45
+ handleInput(data: string): void {
46
+ switch (this.phase) {
47
+ case "instructions":
48
+ if (data === "\r" || data === " ") {
49
+ this.phase = "token";
50
+ } else if (data === "\x1b") {
51
+ this.cleanup();
52
+ this.onClose?.();
53
+ }
54
+ break;
55
+ case "token":
56
+ // Handle bracketed paste mode
57
+ if (this.isInPaste) {
58
+ this.pasteBuffer += data;
59
+ const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
60
+ if (endIndex !== -1) {
61
+ // Extract pasted content and process it
62
+ const pasteContent = this.pasteBuffer.substring(0, endIndex);
63
+ this.processTokenInput(pasteContent);
64
+ this.isInPaste = false;
65
+ this.pasteBuffer = "";
66
+ }
67
+ return;
68
+ }
69
+ // Detect start of bracketed paste
70
+ if (data.includes("\x1b[200~")) {
71
+ this.isInPaste = true;
72
+ this.pasteBuffer = data.replace("\x1b[200~", "");
73
+ return;
74
+ }
75
+ if (data === "\r" && this.botToken.length > 0) {
76
+ this.startPolling();
77
+ } else if (data === "\x1b") {
78
+ this.cleanup();
79
+ this.onClose?.();
80
+ } else if (data === "\x7f" || data === "\b") {
81
+ this.botToken = this.botToken.slice(0, -1);
82
+ } else {
83
+ this.processTokenInput(data);
84
+ }
85
+ break;
86
+ case "polling":
87
+ if (data === "\x1b") {
88
+ this.cleanup();
89
+ this.onClose?.();
90
+ }
91
+ break;
92
+ case "success":
93
+ case "error":
94
+ case "timeout":
95
+ if (data === "\r" || data === " " || data === "\x1b") {
96
+ this.cleanup();
97
+ this.onClose?.();
98
+ }
99
+ break;
100
+ }
101
+ }
102
+
103
+ private processTokenInput(data: string): void {
104
+ // Ignore escape sequences (arrow keys, function keys, etc.)
105
+ if (data.startsWith("\x1b[")) return;
106
+ // Filter to valid bot token characters only
107
+ const cleaned = data.replace(/[^0-9:A-Za-z_-]/g, "");
108
+ if (cleaned.length > 0) {
109
+ this.botToken += cleaned;
110
+ }
111
+ }
112
+
113
+ private startPolling(): void {
114
+ this.phase = "polling";
115
+ this.startTime = Date.now();
116
+ this.pollAbort = new AbortController();
117
+
118
+ // Start spinner animation
119
+ this.spinnerTimer = setInterval(() => {
120
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
121
+ this.requestRender?.();
122
+ }, 80);
123
+
124
+ // Start polling
125
+ this.doPoll();
126
+ }
127
+
128
+ private async doPoll(): Promise<void> {
129
+ if (this.phase !== "polling" || !this.pollAbort) return;
130
+
131
+ try {
132
+ const chatId = await pollForChatId(
133
+ this.botToken,
134
+ this.pollAbort.signal
135
+ );
136
+
137
+ if (chatId) {
138
+ this.chatId = chatId;
139
+ this.phase = "success";
140
+ this.saveConfig();
141
+ this.cleanup();
142
+ this.requestRender?.();
143
+ // Auto-close after brief delay to show success
144
+ setTimeout(() => this.onClose?.(), 1000);
145
+ return;
146
+ }
147
+
148
+ // Check timeout
149
+ if (Date.now() - this.startTime > this.TIMEOUT_MS) {
150
+ this.phase = "timeout";
151
+ this.error = "Timed out after 5 minutes";
152
+ this.cleanup();
153
+ this.requestRender?.();
154
+ return;
155
+ }
156
+
157
+ // Schedule next poll
158
+ this.pollTimer = setTimeout(() => this.doPoll(), 2000);
159
+ } catch (err) {
160
+ if (this.pollAbort?.signal.aborted) return;
161
+ this.phase = "error";
162
+ this.error = err instanceof Error ? err.message : String(err);
163
+ this.cleanup();
164
+ this.requestRender?.();
165
+ }
166
+ }
167
+
168
+ private saveConfig(): void {
169
+ if (this.botToken && this.chatId) {
170
+ updateConfig({
171
+ telegram: {
172
+ enabled: true,
173
+ botToken: this.botToken,
174
+ chatId: this.chatId,
175
+ },
176
+ });
177
+ }
178
+ }
179
+
180
+ private cleanup(): void {
181
+ if (this.spinnerTimer) {
182
+ clearInterval(this.spinnerTimer);
183
+ this.spinnerTimer = null;
184
+ }
185
+ if (this.pollTimer) {
186
+ clearTimeout(this.pollTimer);
187
+ this.pollTimer = null;
188
+ }
189
+ if (this.pollAbort) {
190
+ this.pollAbort.abort();
191
+ this.pollAbort = null;
192
+ }
193
+ }
194
+
195
+ // ─── Theme helpers ───────────────────────────────────────────────────
196
+
197
+ private fg(color: string, text: string): string {
198
+ if (this.theme) return this.theme.fg(color as any, text);
199
+ const c: Record<string, string> = {
200
+ accent: "\x1b[36m", success: "\x1b[32m", warning: "\x1b[33m",
201
+ error: "\x1b[31m", dim: "\x1b[2m", borderMuted: "\x1b[90m",
202
+ };
203
+ return `${c[color] ?? ""}${text}\x1b[0m`;
204
+ }
205
+
206
+ private bold(text: string): string {
207
+ return this.theme ? this.theme.bold(text) : `\x1b[1m${text}\x1b[0m`;
208
+ }
209
+
210
+ private frameLine(content: string, innerWidth: number): string {
211
+ const truncated = truncateToWidth(content, innerWidth, "");
212
+ const padding = Math.max(0, innerWidth - visibleWidth(truncated));
213
+ return `${this.fg("borderMuted", "│")}${truncated}${" ".repeat(padding)}${this.fg("borderMuted", "│")}`;
214
+ }
215
+
216
+ private ruleLine(innerWidth: number): string {
217
+ return this.fg("borderMuted", `├${"─".repeat(innerWidth)}┤`);
218
+ }
219
+
220
+ private borderLine(innerWidth: number, edge: "top" | "bottom"): string {
221
+ const left = edge === "top" ? "┌" : "└";
222
+ const right = edge === "top" ? "┐" : "┘";
223
+ return this.fg("borderMuted", `${left}${"─".repeat(innerWidth)}${right}`);
224
+ }
225
+
226
+ render(width: number): string[] {
227
+ const innerWidth = Math.max(22, width - 2);
228
+ const lines: string[] = [];
229
+
230
+ lines.push(this.borderLine(innerWidth, "top"));
231
+ lines.push(this.frameLine(this.fg("accent", this.bold("🤖 Telegram Bot Setup")), innerWidth));
232
+ lines.push(this.ruleLine(innerWidth));
233
+
234
+ switch (this.phase) {
235
+ case "instructions":
236
+ lines.push(this.frameLine(this.fg("dim", "Set up Telegram notifications in 3 steps:"), innerWidth));
237
+ lines.push(this.frameLine("", innerWidth));
238
+ lines.push(this.frameLine(` ${this.bold("1.")} Open Telegram and message ${this.fg("accent", "@BotFather")}`, innerWidth));
239
+ lines.push(this.frameLine(` Send /newbot and follow the prompts to create a bot`, innerWidth));
240
+ lines.push(this.frameLine("", innerWidth));
241
+ lines.push(this.frameLine(` ${this.bold("2.")} Copy the bot token from BotFather`, innerWidth));
242
+ lines.push(this.frameLine("", innerWidth));
243
+ lines.push(this.frameLine(` ${this.bold("3.")} Send any message to your new bot`, innerWidth));
244
+ lines.push(this.frameLine(` (We'll detect your chat ID automatically)`, innerWidth));
245
+ lines.push(this.ruleLine(innerWidth));
246
+ lines.push(this.frameLine(this.fg("dim", "Press Enter to continue, Esc to cancel"), innerWidth));
247
+ break;
248
+
249
+ case "token":
250
+ lines.push(this.frameLine(this.fg("dim", "Paste your bot token from BotFather:"), innerWidth));
251
+ lines.push(this.frameLine("", innerWidth));
252
+ const display = this.fg("accent", this.bold(this.botToken || " "));
253
+ lines.push(this.frameLine(` ${display}${this.fg("dim", "█")}`, innerWidth));
254
+ lines.push(this.frameLine("", innerWidth));
255
+ lines.push(this.frameLine(this.fg("dim", "Enter to start polling · Esc to cancel"), innerWidth));
256
+ break;
257
+
258
+ case "polling": {
259
+ const frame = SPINNER_FRAMES[this.spinnerFrame] || "⠋";
260
+ const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
261
+ const remaining = Math.max(0, 300 - elapsed);
262
+ lines.push(this.frameLine(` ${this.fg("accent", frame)} ${this.bold("Waiting for first message...")}`, innerWidth));
263
+ lines.push(this.frameLine("", innerWidth));
264
+ lines.push(this.frameLine(` ${this.fg("dim", "Send any message to your bot in Telegram")}`, innerWidth));
265
+ lines.push(this.frameLine(` ${this.fg("dim", `Timeout: ${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, "0")}`)}`, innerWidth));
266
+ lines.push(this.ruleLine(innerWidth));
267
+ lines.push(this.frameLine(this.fg("dim", "Esc to cancel"), innerWidth));
268
+ break;
269
+ }
270
+
271
+ case "success":
272
+ lines.push(this.frameLine(` ${this.fg("success", "✓ Telegram bot configured!")}`, innerWidth));
273
+ lines.push(this.frameLine("", innerWidth));
274
+ lines.push(this.frameLine(` ${this.fg("dim", `Chat ID: ${this.chatId}`)}`, innerWidth));
275
+ lines.push(this.frameLine(` ${this.fg("dim", "Notifications will be sent to this chat")}`, innerWidth));
276
+ lines.push(this.ruleLine(innerWidth));
277
+ lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
278
+ break;
279
+
280
+ case "error":
281
+ lines.push(this.frameLine(` ${this.fg("error", "✗ Setup failed")}`, innerWidth));
282
+ lines.push(this.frameLine("", innerWidth));
283
+ lines.push(this.frameLine(` ${this.fg("dim", this.error || "Unknown error")}`, innerWidth));
284
+ lines.push(this.ruleLine(innerWidth));
285
+ lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
286
+ break;
287
+
288
+ case "timeout":
289
+ lines.push(this.frameLine(` ${this.fg("warning", "⏰ Timed out after 5 minutes")}`, innerWidth));
290
+ lines.push(this.frameLine("", innerWidth));
291
+ lines.push(this.frameLine(` ${this.fg("dim", "Make sure you sent a message to your bot in Telegram")}`, innerWidth));
292
+ lines.push(this.frameLine(` ${this.fg("dim", "You can try again with /unipi:notify-set-tg")}`, innerWidth));
293
+ lines.push(this.ruleLine(innerWidth));
294
+ lines.push(this.frameLine(this.fg("dim", "Press Enter to close"), innerWidth));
295
+ break;
296
+ }
297
+
298
+ lines.push(this.borderLine(innerWidth, "bottom"));
299
+ return lines;
300
+ }
301
+ }
package/types.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @pi-unipi/notify — TypeScript type definitions
3
+ */
4
+
5
+ /** Supported notification platforms */
6
+ export type NotifyPlatform = "native" | "gotify" | "telegram";
7
+
8
+ /** Per-event notification configuration */
9
+ export interface EventNotifyConfig {
10
+ /** Whether this event type is enabled */
11
+ enabled: boolean;
12
+ /** Platforms to send to (empty = use global defaults) */
13
+ platforms: NotifyPlatform[];
14
+ }
15
+
16
+ /** Native notification platform config */
17
+ export interface NativeConfig {
18
+ /** Whether native notifications are enabled */
19
+ enabled: boolean;
20
+ /** Windows appID to show instead of "SnoreToast" */
21
+ windowsAppId?: string;
22
+ }
23
+
24
+ /** Gotify notification platform config */
25
+ export interface GotifyConfig {
26
+ /** Whether Gotify is enabled */
27
+ enabled: boolean;
28
+ /** Gotify server URL */
29
+ serverUrl?: string;
30
+ /** Gotify app token */
31
+ appToken?: string;
32
+ /** Priority level (1-10) */
33
+ priority: number;
34
+ }
35
+
36
+ /** Telegram notification platform config */
37
+ export interface TelegramConfig {
38
+ /** Whether Telegram is enabled */
39
+ enabled: boolean;
40
+ /** Telegram bot token */
41
+ botToken?: string;
42
+ /** Telegram chat ID */
43
+ chatId?: string;
44
+ }
45
+
46
+ /** Full notification configuration */
47
+ export interface NotifyConfig {
48
+ /** Global default platforms for all events */
49
+ defaultPlatforms: NotifyPlatform[];
50
+ /** Per-event type overrides */
51
+ events: Record<string, EventNotifyConfig>;
52
+ /** Native platform settings */
53
+ native: NativeConfig;
54
+ /** Gotify settings */
55
+ gotify: GotifyConfig;
56
+ /** Telegram settings */
57
+ telegram: TelegramConfig;
58
+ }
59
+
60
+ /** Parameters for the notify_user agent tool */
61
+ export interface NotifyUserParams {
62
+ /** Notification message body */
63
+ message: string;
64
+ /** Notification title (default: "Pi Notification") */
65
+ title?: string;
66
+ /** Priority level */
67
+ priority?: "low" | "normal" | "high";
68
+ /** Override platforms for this notification */
69
+ platforms?: NotifyPlatform[];
70
+ }
71
+
72
+ /** Result of sending a notification to a single platform */
73
+ export interface NotifyResult {
74
+ /** Platform that was targeted */
75
+ platform: NotifyPlatform;
76
+ /** Whether the send succeeded */
77
+ success: boolean;
78
+ /** Error message if failed */
79
+ error?: string;
80
+ }
81
+
82
+ /** Notification dispatch summary */
83
+ export interface NotifyDispatchResult {
84
+ /** Results per platform */
85
+ results: NotifyResult[];
86
+ /** Whether all platforms succeeded */
87
+ allSuccess: boolean;
88
+ }