pi-smart-voice-notify 0.1.0
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/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/assets/Machine-alert-beep-sound-effect.mp3 +0 -0
- package/assets/Soft-high-tech-notification-sound-effect.mp3 +0 -0
- package/assets/pi-smart-voice-notify.png +0 -0
- package/config/config.example.json +30 -0
- package/index.ts +3 -0
- package/package.json +53 -0
- package/src/config-store.ts +308 -0
- package/src/desktop-notify.ts +177 -0
- package/src/index.ts +860 -0
- package/src/logging.ts +73 -0
- package/src/notify-audio.ts +414 -0
- package/src/types.ts +52 -0
- package/src/zellij-modal.ts +999 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
type NotificationType = "idle" | "permission" | "question" | "error";
|
|
2
|
+
|
|
3
|
+
type LinuxUrgency = "low" | "normal" | "critical";
|
|
4
|
+
|
|
5
|
+
interface DesktopNotificationSupport {
|
|
6
|
+
supported: boolean;
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface DesktopNotificationRequest {
|
|
11
|
+
type: NotificationType;
|
|
12
|
+
message: string;
|
|
13
|
+
timeoutSeconds: number;
|
|
14
|
+
debugLog?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DesktopNotificationResult {
|
|
18
|
+
success: boolean;
|
|
19
|
+
platform: NodeJS.Platform;
|
|
20
|
+
unsupported?: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface NotifierLike {
|
|
25
|
+
notify(
|
|
26
|
+
options: Record<string, unknown>,
|
|
27
|
+
callback?: (error: Error | null, response?: unknown, metadata?: unknown) => void,
|
|
28
|
+
): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TITLES: Record<NotificationType, string> = {
|
|
32
|
+
idle: "✅ Pi - Task Complete",
|
|
33
|
+
permission: "⚠️ Pi - Permission Required",
|
|
34
|
+
question: "❓ Pi - Input Needed",
|
|
35
|
+
error: "❌ Pi - Error",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const LINUX_URGENCY: Record<NotificationType, LinuxUrgency> = {
|
|
39
|
+
idle: "normal",
|
|
40
|
+
permission: "critical",
|
|
41
|
+
question: "normal",
|
|
42
|
+
error: "critical",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let notifierPromise: Promise<NotifierLike | null> | null = null;
|
|
46
|
+
|
|
47
|
+
function getErrorMessage(error: unknown): string {
|
|
48
|
+
if (error instanceof Error) {
|
|
49
|
+
return error.message;
|
|
50
|
+
}
|
|
51
|
+
return String(error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function clampTimeoutSeconds(value: number): number {
|
|
55
|
+
if (!Number.isFinite(value)) {
|
|
56
|
+
return 5;
|
|
57
|
+
}
|
|
58
|
+
return Math.min(60, Math.max(1, Math.trunc(value)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function checkDesktopNotificationSupport(platform = process.platform): DesktopNotificationSupport {
|
|
62
|
+
switch (platform) {
|
|
63
|
+
case "win32":
|
|
64
|
+
case "darwin":
|
|
65
|
+
case "linux":
|
|
66
|
+
return { supported: true };
|
|
67
|
+
default:
|
|
68
|
+
return {
|
|
69
|
+
supported: false,
|
|
70
|
+
reason: `Desktop notifications are unsupported on platform '${platform}'.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildNotifierOptions(request: DesktopNotificationRequest): Record<string, unknown> {
|
|
76
|
+
const timeoutSeconds = clampTimeoutSeconds(request.timeoutSeconds);
|
|
77
|
+
const baseOptions: Record<string, unknown> = {
|
|
78
|
+
title: TITLES[request.type],
|
|
79
|
+
message: request.message,
|
|
80
|
+
wait: false,
|
|
81
|
+
timeout: timeoutSeconds,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (process.platform === "linux") {
|
|
85
|
+
baseOptions.urgency = LINUX_URGENCY[request.type];
|
|
86
|
+
baseOptions["app-name"] = "Pi Smart Voice Notify";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (process.platform === "win32") {
|
|
90
|
+
baseOptions.appID = "PiSmartVoiceNotify";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (process.platform === "darwin") {
|
|
94
|
+
baseOptions.subtitle = "Smart Voice Notify";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return baseOptions;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function getNotifier(): Promise<NotifierLike | null> {
|
|
101
|
+
if (!notifierPromise) {
|
|
102
|
+
notifierPromise = import("node-notifier")
|
|
103
|
+
.then((module) => {
|
|
104
|
+
const candidate = (module.default ?? module) as { notify?: NotifierLike["notify"] };
|
|
105
|
+
if (typeof candidate.notify !== "function") {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return { notify: candidate.notify };
|
|
109
|
+
})
|
|
110
|
+
.catch(() => null);
|
|
111
|
+
}
|
|
112
|
+
return notifierPromise;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function sendDesktopNotification(request: DesktopNotificationRequest): Promise<DesktopNotificationResult> {
|
|
116
|
+
const support = checkDesktopNotificationSupport();
|
|
117
|
+
if (!support.supported) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
platform: process.platform,
|
|
121
|
+
unsupported: true,
|
|
122
|
+
error: support.reason,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const notifier = await getNotifier();
|
|
127
|
+
if (!notifier) {
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
platform: process.platform,
|
|
131
|
+
error: "node-notifier is not available. Install it in this extension directory.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const notifyOptions = buildNotifierOptions(request);
|
|
136
|
+
const timeoutSeconds = clampTimeoutSeconds(request.timeoutSeconds);
|
|
137
|
+
const callbackTimeoutMs = Math.min(1200, Math.max(250, timeoutSeconds * 1000 + 250));
|
|
138
|
+
|
|
139
|
+
return new Promise<DesktopNotificationResult>((resolve) => {
|
|
140
|
+
let settled = false;
|
|
141
|
+
const settle = (result: DesktopNotificationResult): void => {
|
|
142
|
+
if (settled) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
settled = true;
|
|
146
|
+
clearTimeout(safetyTimeout);
|
|
147
|
+
resolve(result);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const safetyTimeout = setTimeout(() => {
|
|
151
|
+
if (request.debugLog) {
|
|
152
|
+
// Callback can be dropped by some notifier backends; treat as queued/success.
|
|
153
|
+
}
|
|
154
|
+
settle({ success: true, platform: process.platform });
|
|
155
|
+
}, callbackTimeoutMs);
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
notifier.notify(notifyOptions, (error) => {
|
|
159
|
+
if (error) {
|
|
160
|
+
settle({
|
|
161
|
+
success: false,
|
|
162
|
+
platform: process.platform,
|
|
163
|
+
error: getErrorMessage(error),
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
settle({ success: true, platform: process.platform });
|
|
168
|
+
});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
settle({
|
|
171
|
+
success: false,
|
|
172
|
+
platform: process.platform,
|
|
173
|
+
error: getErrorMessage(error),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|