@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.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/command.d.ts +2 -0
- package/dist/command.js +25 -0
- package/dist/config.d.ts +81 -0
- package/dist/config.js +245 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4598 -0
- package/dist/logos/opencode-logo-dark.png +0 -0
- package/dist/logos/opencode-logo-light.png +0 -0
- package/dist/notify.d.ts +1 -0
- package/dist/notify.js +124 -0
- package/dist/sound.d.ts +2 -0
- package/dist/sound.js +127 -0
- package/dist/sounds/complete.wav +0 -0
- package/dist/sounds/error.wav +0 -0
- package/dist/sounds/permission.wav +0 -0
- package/dist/sounds/question.wav +0 -0
- package/dist/sounds/subagent_complete.wav +0 -0
- package/dist/wechat-webhook.d.ts +8 -0
- package/dist/wechat-webhook.js +43 -0
- package/logos/opencode-logo-dark.png +0 -0
- package/logos/opencode-logo-light.png +0 -0
- package/package.json +48 -0
- package/sounds/complete.wav +0 -0
- package/sounds/error.wav +0 -0
- package/sounds/permission.wav +0 -0
- package/sounds/question.wav +0 -0
- package/sounds/subagent_complete.wav +0 -0
|
Binary file
|
|
Binary file
|
package/dist/notify.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/sound.d.ts
ADDED
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
|
|
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
|
package/sounds/error.wav
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|