@thelastwinner/opencode-notifier 0.1.30

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 @@
1
+ export declare function sendNotification(title: string, message: string, timeout: number, iconPath?: string, notificationSystem?: "osascript" | "node-notifier", linuxGrouping?: boolean): Promise<void>;
package/dist/notify.js ADDED
@@ -0,0 +1,124 @@
1
+ // src/notify.ts
2
+ import os from "os";
3
+ import { exec, execFile } from "child_process";
4
+ import notifier from "node-notifier";
5
+ var DEBOUNCE_MS = 1e3;
6
+ var platform = os.type();
7
+ var platformNotifier;
8
+ if (platform === "Linux" || platform.match(/BSD$/)) {
9
+ const { NotifySend } = notifier;
10
+ platformNotifier = new NotifySend({ withFallback: false });
11
+ } else if (platform === "Windows_NT") {
12
+ const { WindowsToaster } = notifier;
13
+ platformNotifier = new WindowsToaster({ withFallback: false });
14
+ } else if (platform !== "Darwin") {
15
+ platformNotifier = notifier;
16
+ }
17
+ var lastNotificationTime = {};
18
+ var lastLinuxNotificationId = null;
19
+ var linuxNotifySendSupportsReplace = null;
20
+ function detectNotifySendCapabilities() {
21
+ return new Promise((resolve) => {
22
+ execFile("notify-send", ["--version"], (error, stdout) => {
23
+ if (error) {
24
+ resolve(false);
25
+ return;
26
+ }
27
+ const match = stdout.match(/(\d+)\.(\d+)/);
28
+ if (match) {
29
+ const major = parseInt(match[1], 10);
30
+ const minor = parseInt(match[2], 10);
31
+ resolve(major > 0 || major === 0 && minor >= 8);
32
+ return;
33
+ }
34
+ resolve(false);
35
+ });
36
+ });
37
+ }
38
+ function sendLinuxNotificationDirect(title, message, timeout, iconPath, grouping = true) {
39
+ return new Promise((resolve) => {
40
+ const args = [];
41
+ if (iconPath) {
42
+ args.push("--icon", iconPath);
43
+ }
44
+ args.push("--expire-time", String(timeout * 1e3));
45
+ if (grouping && lastLinuxNotificationId !== null) {
46
+ args.push("--replace-id", String(lastLinuxNotificationId));
47
+ }
48
+ if (grouping) {
49
+ args.push("--print-id");
50
+ }
51
+ args.push("--", title, message);
52
+ execFile("notify-send", args, (error, stdout) => {
53
+ if (!error && grouping && stdout) {
54
+ const id = parseInt(stdout.trim(), 10);
55
+ if (!isNaN(id)) {
56
+ lastLinuxNotificationId = id;
57
+ }
58
+ }
59
+ resolve();
60
+ });
61
+ });
62
+ }
63
+ async function sendNotification(title, message, timeout, iconPath, notificationSystem = "osascript", linuxGrouping = true) {
64
+ const now = Date.now();
65
+ if (lastNotificationTime[message] && now - lastNotificationTime[message] < DEBOUNCE_MS) {
66
+ return;
67
+ }
68
+ lastNotificationTime[message] = now;
69
+ if (platform === "Darwin") {
70
+ if (notificationSystem === "node-notifier") {
71
+ return new Promise((resolve) => {
72
+ const notificationOptions = {
73
+ title,
74
+ message,
75
+ timeout,
76
+ icon: iconPath
77
+ };
78
+ notifier.notify(
79
+ notificationOptions,
80
+ () => {
81
+ resolve();
82
+ }
83
+ );
84
+ });
85
+ }
86
+ return new Promise((resolve) => {
87
+ const escapedMessage = message.replace(/"/g, '\\"');
88
+ const escapedTitle = title.replace(/"/g, '\\"');
89
+ exec(
90
+ `osascript -e 'display notification "${escapedMessage}" with title "${escapedTitle}"'`,
91
+ () => {
92
+ resolve();
93
+ }
94
+ );
95
+ });
96
+ }
97
+ if (platform === "Linux" || platform.match(/BSD$/)) {
98
+ if (linuxGrouping) {
99
+ if (linuxNotifySendSupportsReplace === null) {
100
+ linuxNotifySendSupportsReplace = await detectNotifySendCapabilities();
101
+ }
102
+ if (linuxNotifySendSupportsReplace) {
103
+ return sendLinuxNotificationDirect(title, message, timeout, iconPath, true);
104
+ }
105
+ }
106
+ }
107
+ return new Promise((resolve) => {
108
+ const notificationOptions = {
109
+ title,
110
+ message,
111
+ timeout,
112
+ icon: iconPath
113
+ };
114
+ platformNotifier.notify(
115
+ notificationOptions,
116
+ () => {
117
+ resolve();
118
+ }
119
+ );
120
+ });
121
+ }
122
+ export {
123
+ sendNotification
124
+ };
@@ -0,0 +1,2 @@
1
+ import type { EventType } from "./config";
2
+ export declare function playSound(event: EventType, customPath: string | null, volume: number): Promise<void>;
package/dist/sound.js ADDED
@@ -0,0 +1,127 @@
1
+ // src/sound.ts
2
+ import { platform } from "os";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { existsSync } from "fs";
6
+ import { spawn } from "child_process";
7
+ var __dirname = dirname(fileURLToPath(import.meta.url));
8
+ var DEBOUNCE_MS = 1e3;
9
+ var FULL_VOLUME_PERCENT = 100;
10
+ var FULL_VOLUME_PULSE = 65536;
11
+ var lastSoundTime = {};
12
+ function getBundledSoundPath(event) {
13
+ const soundFilename = `${event}.wav`;
14
+ const possiblePaths = [
15
+ join(__dirname, "..", "sounds", soundFilename),
16
+ join(__dirname, "sounds", soundFilename)
17
+ ];
18
+ for (const path of possiblePaths) {
19
+ if (existsSync(path)) {
20
+ return path;
21
+ }
22
+ }
23
+ return join(__dirname, "..", "sounds", soundFilename);
24
+ }
25
+ function getSoundFilePath(event, customPath) {
26
+ if (customPath && existsSync(customPath)) {
27
+ return customPath;
28
+ }
29
+ const bundledPath = getBundledSoundPath(event);
30
+ if (existsSync(bundledPath)) {
31
+ return bundledPath;
32
+ }
33
+ return null;
34
+ }
35
+ async function runCommand(command, args) {
36
+ return new Promise((resolve, reject) => {
37
+ const proc = spawn(command, args, {
38
+ stdio: "ignore",
39
+ detached: false
40
+ });
41
+ proc.on("error", (err) => {
42
+ reject(err);
43
+ });
44
+ proc.on("close", (code) => {
45
+ if (code === 0) {
46
+ resolve();
47
+ } else {
48
+ reject(new Error(`Command exited with code ${code}`));
49
+ }
50
+ });
51
+ });
52
+ }
53
+ function normalizeVolume(volume) {
54
+ if (!Number.isFinite(volume)) {
55
+ return 1;
56
+ }
57
+ if (volume < 0) {
58
+ return 0;
59
+ }
60
+ if (volume > 1) {
61
+ return 1;
62
+ }
63
+ return volume;
64
+ }
65
+ function toPercentVolume(volume) {
66
+ return Math.round(volume * FULL_VOLUME_PERCENT);
67
+ }
68
+ function toPulseVolume(volume) {
69
+ return Math.round(volume * FULL_VOLUME_PULSE);
70
+ }
71
+ async function playOnLinux(soundPath, volume) {
72
+ const percentVolume = toPercentVolume(volume);
73
+ const pulseVolume = toPulseVolume(volume);
74
+ const players = [
75
+ { command: "paplay", args: [`--volume=${pulseVolume}`, soundPath] },
76
+ { command: "aplay", args: [soundPath] },
77
+ { command: "mpv", args: ["--no-video", "--no-terminal", `--volume=${percentVolume}`, soundPath] },
78
+ { command: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", "-volume", `${percentVolume}`, soundPath] }
79
+ ];
80
+ for (const player of players) {
81
+ try {
82
+ await runCommand(player.command, player.args);
83
+ return;
84
+ } catch {
85
+ continue;
86
+ }
87
+ }
88
+ }
89
+ async function playOnMac(soundPath, volume) {
90
+ await runCommand("afplay", ["-v", `${volume}`, soundPath]);
91
+ }
92
+ async function playOnWindows(soundPath) {
93
+ const script = `& { (New-Object Media.SoundPlayer $args[0]).PlaySync() }`;
94
+ await runCommand("powershell", ["-c", script, soundPath]);
95
+ }
96
+ async function playSound(event, customPath, volume) {
97
+ const now = Date.now();
98
+ if (lastSoundTime[event] && now - lastSoundTime[event] < DEBOUNCE_MS) {
99
+ return;
100
+ }
101
+ lastSoundTime[event] = now;
102
+ const soundPath = getSoundFilePath(event, customPath);
103
+ const normalizedVolume = normalizeVolume(volume);
104
+ if (!soundPath) {
105
+ return;
106
+ }
107
+ const os = platform();
108
+ try {
109
+ switch (os) {
110
+ case "darwin":
111
+ await playOnMac(soundPath, normalizedVolume);
112
+ break;
113
+ case "linux":
114
+ await playOnLinux(soundPath, normalizedVolume);
115
+ break;
116
+ case "win32":
117
+ await playOnWindows(soundPath);
118
+ break;
119
+ default:
120
+ break;
121
+ }
122
+ } catch {
123
+ }
124
+ }
125
+ export {
126
+ playSound
127
+ };
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,8 @@
1
+ interface WechatWebhookConfig {
2
+ enabled: boolean;
3
+ webhookUrl: string;
4
+ mentionedList?: string[];
5
+ mentionedMobileList?: string[];
6
+ }
7
+ export declare function sendWechatWebhook(config: WechatWebhookConfig, title: string, message: string): Promise<void>;
8
+ export {};
@@ -0,0 +1,43 @@
1
+ // src/wechat-webhook.ts
2
+ async function sendWebhookRequest(url, message) {
3
+ try {
4
+ const response = await fetch(url, {
5
+ method: "POST",
6
+ headers: {
7
+ "Content-Type": "application/json; charset=utf-8"
8
+ },
9
+ body: JSON.stringify(message)
10
+ });
11
+ if (!response.ok) {
12
+ return false;
13
+ }
14
+ const data = await response.json();
15
+ return data.errcode === 0;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+ async function sendWechatWebhook(config, title, message) {
21
+ if (!config.enabled || !config.webhookUrl) {
22
+ return;
23
+ }
24
+ const fullMessage = `${title}
25
+
26
+ ${message}`;
27
+ const webhookMessage = {
28
+ msgtype: "text",
29
+ text: {
30
+ content: fullMessage
31
+ }
32
+ };
33
+ if (config.mentionedList && config.mentionedList.length > 0) {
34
+ webhookMessage.text.mentioned_list = config.mentionedList;
35
+ }
36
+ if (config.mentionedMobileList && config.mentionedMobileList.length > 0) {
37
+ webhookMessage.text.mentioned_mobile_list = config.mentionedMobileList;
38
+ }
39
+ await sendWebhookRequest(config.webhookUrl, webhookMessage);
40
+ }
41
+ export {
42
+ sendWechatWebhook
43
+ };
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@thelastwinner/opencode-notifier",
3
+ "version": "0.1.30",
4
+ "description": "OpenCode plugin that sends system notifications and plays sounds when permission is needed, generation completes, or errors occur",
5
+ "author": "mohak34",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/mohak34/opencode-notifier.git"
13
+ },
14
+ "homepage": "https://github.com/mohak34/opencode-notifier#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/mohak34/opencode-notifier/issues"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "sounds",
21
+ "logos"
22
+ ],
23
+ "scripts": {
24
+ "build": "bun build src/index.ts --outdir dist --target node",
25
+ "prepublishOnly": "bun run build",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "bun test"
28
+ },
29
+ "keywords": [
30
+ "opencode",
31
+ "opencode-plugin",
32
+ "notifications",
33
+ "sound",
34
+ "alerts"
35
+ ],
36
+ "dependencies": {
37
+ "node-notifier": "^10.0.1"
38
+ },
39
+ "devDependencies": {
40
+ "@opencode-ai/plugin": "^1.0.224",
41
+ "@types/node": "^22.0.0",
42
+ "@types/node-notifier": "^8.0.5",
43
+ "typescript": "^5.0.0"
44
+ },
45
+ "peerDependencies": {
46
+ "@opencode-ai/plugin": ">=1.0.0"
47
+ }
48
+ }
Binary file
Binary file
Binary file
Binary file
Binary file