@mohak34/opencode-notifier 0.1.21-beta.0 → 0.1.22-beta.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/README.md +29 -2
- package/dist/index.js +115 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
|
|
|
52
52
|
"notification": true,
|
|
53
53
|
"timeout": 5,
|
|
54
54
|
"showProjectName": true,
|
|
55
|
-
"showSessionTitle":
|
|
55
|
+
"showSessionTitle": false,
|
|
56
56
|
"showIcon": true,
|
|
57
57
|
"notificationSystem": "osascript",
|
|
58
58
|
"command": {
|
|
@@ -81,6 +81,13 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
|
|
|
81
81
|
"subagent_complete": null,
|
|
82
82
|
"error": null,
|
|
83
83
|
"question": null
|
|
84
|
+
},
|
|
85
|
+
"volumes": {
|
|
86
|
+
"permission": 1,
|
|
87
|
+
"complete": 1,
|
|
88
|
+
"subagent_complete": 1,
|
|
89
|
+
"error": 1,
|
|
90
|
+
"question": 1
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
```
|
|
@@ -95,7 +102,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
|
|
|
95
102
|
"notification": true,
|
|
96
103
|
"timeout": 5,
|
|
97
104
|
"showProjectName": true,
|
|
98
|
-
"showSessionTitle":
|
|
105
|
+
"showSessionTitle": false,
|
|
99
106
|
"showIcon": true,
|
|
100
107
|
"notificationSystem": "osascript"
|
|
101
108
|
}
|
|
@@ -181,6 +188,26 @@ Platform notes:
|
|
|
181
188
|
- Windows: Only .wav files work
|
|
182
189
|
- If file doesn't exist, falls back to bundled sound
|
|
183
190
|
|
|
191
|
+
### Volumes
|
|
192
|
+
|
|
193
|
+
Set per-event volume from `0` to `1`:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"volumes": {
|
|
198
|
+
"permission": 0.6,
|
|
199
|
+
"complete": 0.3,
|
|
200
|
+
"subagent_complete": 0.15,
|
|
201
|
+
"error": 1,
|
|
202
|
+
"question": 0.7
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
- `0` = mute, `1` = full volume
|
|
208
|
+
- Values outside `0..1` are clamped automatically
|
|
209
|
+
- On Windows, playback still works but custom volume may not be honored by the default player
|
|
210
|
+
|
|
184
211
|
### Custom commands
|
|
185
212
|
|
|
186
213
|
Run your own script when something happens. Use `{event}`, `{message}`, and `{sessionTitle}` as placeholders:
|
package/dist/index.js
CHANGED
|
@@ -3743,7 +3743,7 @@ var DEFAULT_CONFIG = {
|
|
|
3743
3743
|
notification: true,
|
|
3744
3744
|
timeout: 5,
|
|
3745
3745
|
showProjectName: true,
|
|
3746
|
-
showSessionTitle:
|
|
3746
|
+
showSessionTitle: false,
|
|
3747
3747
|
showIcon: true,
|
|
3748
3748
|
notificationSystem: "osascript",
|
|
3749
3749
|
command: {
|
|
@@ -3756,21 +3756,32 @@ var DEFAULT_CONFIG = {
|
|
|
3756
3756
|
complete: { ...DEFAULT_EVENT_CONFIG },
|
|
3757
3757
|
subagent_complete: { sound: false, notification: false },
|
|
3758
3758
|
error: { ...DEFAULT_EVENT_CONFIG },
|
|
3759
|
-
question: { ...DEFAULT_EVENT_CONFIG }
|
|
3759
|
+
question: { ...DEFAULT_EVENT_CONFIG },
|
|
3760
|
+
interrupted: { ...DEFAULT_EVENT_CONFIG }
|
|
3760
3761
|
},
|
|
3761
3762
|
messages: {
|
|
3762
3763
|
permission: "Session needs permission: {sessionTitle}",
|
|
3763
3764
|
complete: "Session has finished: {sessionTitle}",
|
|
3764
3765
|
subagent_complete: "Subagent task completed: {sessionTitle}",
|
|
3765
3766
|
error: "Session encountered an error: {sessionTitle}",
|
|
3766
|
-
question: "Session has a question: {sessionTitle}"
|
|
3767
|
+
question: "Session has a question: {sessionTitle}",
|
|
3768
|
+
interrupted: "Session was interrupted: {sessionTitle}"
|
|
3767
3769
|
},
|
|
3768
3770
|
sounds: {
|
|
3769
3771
|
permission: null,
|
|
3770
3772
|
complete: null,
|
|
3771
3773
|
subagent_complete: null,
|
|
3772
3774
|
error: null,
|
|
3773
|
-
question: null
|
|
3775
|
+
question: null,
|
|
3776
|
+
interrupted: null
|
|
3777
|
+
},
|
|
3778
|
+
volumes: {
|
|
3779
|
+
permission: 1,
|
|
3780
|
+
complete: 1,
|
|
3781
|
+
subagent_complete: 1,
|
|
3782
|
+
error: 1,
|
|
3783
|
+
question: 1,
|
|
3784
|
+
interrupted: 1
|
|
3774
3785
|
}
|
|
3775
3786
|
};
|
|
3776
3787
|
function getConfigPath() {
|
|
@@ -3791,6 +3802,18 @@ function parseEventConfig(userEvent, defaultConfig) {
|
|
|
3791
3802
|
notification: userEvent.notification ?? defaultConfig.notification
|
|
3792
3803
|
};
|
|
3793
3804
|
}
|
|
3805
|
+
function parseVolume(value, defaultVolume) {
|
|
3806
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3807
|
+
return defaultVolume;
|
|
3808
|
+
}
|
|
3809
|
+
if (value < 0) {
|
|
3810
|
+
return 0;
|
|
3811
|
+
}
|
|
3812
|
+
if (value > 1) {
|
|
3813
|
+
return 1;
|
|
3814
|
+
}
|
|
3815
|
+
return value;
|
|
3816
|
+
}
|
|
3794
3817
|
function loadConfig() {
|
|
3795
3818
|
const configPath = getConfigPath();
|
|
3796
3819
|
if (!existsSync(configPath)) {
|
|
@@ -3827,21 +3850,32 @@ function loadConfig() {
|
|
|
3827
3850
|
complete: parseEventConfig(userConfig.events?.complete ?? userConfig.complete, defaultWithGlobal),
|
|
3828
3851
|
subagent_complete: parseEventConfig(userConfig.events?.subagent_complete ?? userConfig.subagent_complete, { sound: false, notification: false }),
|
|
3829
3852
|
error: parseEventConfig(userConfig.events?.error ?? userConfig.error, defaultWithGlobal),
|
|
3830
|
-
question: parseEventConfig(userConfig.events?.question ?? userConfig.question, defaultWithGlobal)
|
|
3853
|
+
question: parseEventConfig(userConfig.events?.question ?? userConfig.question, defaultWithGlobal),
|
|
3854
|
+
interrupted: parseEventConfig(userConfig.events?.interrupted ?? userConfig.interrupted, defaultWithGlobal)
|
|
3831
3855
|
},
|
|
3832
3856
|
messages: {
|
|
3833
3857
|
permission: userConfig.messages?.permission ?? DEFAULT_CONFIG.messages.permission,
|
|
3834
3858
|
complete: userConfig.messages?.complete ?? DEFAULT_CONFIG.messages.complete,
|
|
3835
3859
|
subagent_complete: userConfig.messages?.subagent_complete ?? DEFAULT_CONFIG.messages.subagent_complete,
|
|
3836
3860
|
error: userConfig.messages?.error ?? DEFAULT_CONFIG.messages.error,
|
|
3837
|
-
question: userConfig.messages?.question ?? DEFAULT_CONFIG.messages.question
|
|
3861
|
+
question: userConfig.messages?.question ?? DEFAULT_CONFIG.messages.question,
|
|
3862
|
+
interrupted: userConfig.messages?.interrupted ?? DEFAULT_CONFIG.messages.interrupted
|
|
3838
3863
|
},
|
|
3839
3864
|
sounds: {
|
|
3840
3865
|
permission: userConfig.sounds?.permission ?? DEFAULT_CONFIG.sounds.permission,
|
|
3841
3866
|
complete: userConfig.sounds?.complete ?? DEFAULT_CONFIG.sounds.complete,
|
|
3842
3867
|
subagent_complete: userConfig.sounds?.subagent_complete ?? DEFAULT_CONFIG.sounds.subagent_complete,
|
|
3843
3868
|
error: userConfig.sounds?.error ?? DEFAULT_CONFIG.sounds.error,
|
|
3844
|
-
question: userConfig.sounds?.question ?? DEFAULT_CONFIG.sounds.question
|
|
3869
|
+
question: userConfig.sounds?.question ?? DEFAULT_CONFIG.sounds.question,
|
|
3870
|
+
interrupted: userConfig.sounds?.interrupted ?? DEFAULT_CONFIG.sounds.interrupted
|
|
3871
|
+
},
|
|
3872
|
+
volumes: {
|
|
3873
|
+
permission: parseVolume(userConfig.volumes?.permission, DEFAULT_CONFIG.volumes.permission),
|
|
3874
|
+
complete: parseVolume(userConfig.volumes?.complete, DEFAULT_CONFIG.volumes.complete),
|
|
3875
|
+
subagent_complete: parseVolume(userConfig.volumes?.subagent_complete, DEFAULT_CONFIG.volumes.subagent_complete),
|
|
3876
|
+
error: parseVolume(userConfig.volumes?.error, DEFAULT_CONFIG.volumes.error),
|
|
3877
|
+
question: parseVolume(userConfig.volumes?.question, DEFAULT_CONFIG.volumes.question),
|
|
3878
|
+
interrupted: parseVolume(userConfig.volumes?.interrupted, DEFAULT_CONFIG.volumes.interrupted)
|
|
3845
3879
|
}
|
|
3846
3880
|
};
|
|
3847
3881
|
} catch {
|
|
@@ -3860,6 +3894,9 @@ function getMessage(config, event) {
|
|
|
3860
3894
|
function getSoundPath(config, event) {
|
|
3861
3895
|
return config.sounds[event];
|
|
3862
3896
|
}
|
|
3897
|
+
function getSoundVolume(config, event) {
|
|
3898
|
+
return config.volumes[event];
|
|
3899
|
+
}
|
|
3863
3900
|
function getIconPath(config) {
|
|
3864
3901
|
if (!config.showIcon) {
|
|
3865
3902
|
return;
|
|
@@ -3951,6 +3988,8 @@ import { existsSync as existsSync2 } from "fs";
|
|
|
3951
3988
|
import { spawn } from "child_process";
|
|
3952
3989
|
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
3953
3990
|
var DEBOUNCE_MS2 = 1000;
|
|
3991
|
+
var FULL_VOLUME_PERCENT = 100;
|
|
3992
|
+
var FULL_VOLUME_PULSE = 65536;
|
|
3954
3993
|
var lastSoundTime = {};
|
|
3955
3994
|
function getBundledSoundPath(event) {
|
|
3956
3995
|
const soundFilename = `${event}.wav`;
|
|
@@ -3993,12 +4032,32 @@ async function runCommand(command, args) {
|
|
|
3993
4032
|
});
|
|
3994
4033
|
});
|
|
3995
4034
|
}
|
|
3996
|
-
|
|
4035
|
+
function normalizeVolume(volume) {
|
|
4036
|
+
if (!Number.isFinite(volume)) {
|
|
4037
|
+
return 1;
|
|
4038
|
+
}
|
|
4039
|
+
if (volume < 0) {
|
|
4040
|
+
return 0;
|
|
4041
|
+
}
|
|
4042
|
+
if (volume > 1) {
|
|
4043
|
+
return 1;
|
|
4044
|
+
}
|
|
4045
|
+
return volume;
|
|
4046
|
+
}
|
|
4047
|
+
function toPercentVolume(volume) {
|
|
4048
|
+
return Math.round(volume * FULL_VOLUME_PERCENT);
|
|
4049
|
+
}
|
|
4050
|
+
function toPulseVolume(volume) {
|
|
4051
|
+
return Math.round(volume * FULL_VOLUME_PULSE);
|
|
4052
|
+
}
|
|
4053
|
+
async function playOnLinux(soundPath, volume) {
|
|
4054
|
+
const percentVolume = toPercentVolume(volume);
|
|
4055
|
+
const pulseVolume = toPulseVolume(volume);
|
|
3997
4056
|
const players = [
|
|
3998
|
-
{ command: "paplay", args: [soundPath] },
|
|
4057
|
+
{ command: "paplay", args: [`--volume=${pulseVolume}`, soundPath] },
|
|
3999
4058
|
{ command: "aplay", args: [soundPath] },
|
|
4000
|
-
{ command: "mpv", args: ["--no-video", "--no-terminal", soundPath] },
|
|
4001
|
-
{ command: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", soundPath] }
|
|
4059
|
+
{ command: "mpv", args: ["--no-video", "--no-terminal", `--volume=${percentVolume}`, soundPath] },
|
|
4060
|
+
{ command: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", "-volume", `${percentVolume}`, soundPath] }
|
|
4002
4061
|
];
|
|
4003
4062
|
for (const player of players) {
|
|
4004
4063
|
try {
|
|
@@ -4009,20 +4068,21 @@ async function playOnLinux(soundPath) {
|
|
|
4009
4068
|
}
|
|
4010
4069
|
}
|
|
4011
4070
|
}
|
|
4012
|
-
async function playOnMac(soundPath) {
|
|
4013
|
-
await runCommand("afplay", [soundPath]);
|
|
4071
|
+
async function playOnMac(soundPath, volume) {
|
|
4072
|
+
await runCommand("afplay", ["-v", `${volume}`, soundPath]);
|
|
4014
4073
|
}
|
|
4015
4074
|
async function playOnWindows(soundPath) {
|
|
4016
4075
|
const script = `& { (New-Object Media.SoundPlayer $args[0]).PlaySync() }`;
|
|
4017
4076
|
await runCommand("powershell", ["-c", script, soundPath]);
|
|
4018
4077
|
}
|
|
4019
|
-
async function playSound(event, customPath) {
|
|
4078
|
+
async function playSound(event, customPath, volume) {
|
|
4020
4079
|
const now = Date.now();
|
|
4021
4080
|
if (lastSoundTime[event] && now - lastSoundTime[event] < DEBOUNCE_MS2) {
|
|
4022
4081
|
return;
|
|
4023
4082
|
}
|
|
4024
4083
|
lastSoundTime[event] = now;
|
|
4025
4084
|
const soundPath = getSoundFilePath(event, customPath);
|
|
4085
|
+
const normalizedVolume = normalizeVolume(volume);
|
|
4026
4086
|
if (!soundPath) {
|
|
4027
4087
|
return;
|
|
4028
4088
|
}
|
|
@@ -4030,10 +4090,10 @@ async function playSound(event, customPath) {
|
|
|
4030
4090
|
try {
|
|
4031
4091
|
switch (os2) {
|
|
4032
4092
|
case "darwin":
|
|
4033
|
-
await playOnMac(soundPath);
|
|
4093
|
+
await playOnMac(soundPath, normalizedVolume);
|
|
4034
4094
|
break;
|
|
4035
4095
|
case "linux":
|
|
4036
|
-
await playOnLinux(soundPath);
|
|
4096
|
+
await playOnLinux(soundPath, normalizedVolume);
|
|
4037
4097
|
break;
|
|
4038
4098
|
case "win32":
|
|
4039
4099
|
await playOnWindows(soundPath);
|
|
@@ -4067,6 +4127,15 @@ function runCommand2(config, event, message, sessionTitle, projectName) {
|
|
|
4067
4127
|
}
|
|
4068
4128
|
|
|
4069
4129
|
// src/index.ts
|
|
4130
|
+
var recentErrors = new Map;
|
|
4131
|
+
setInterval(() => {
|
|
4132
|
+
const cutoff = Date.now() - 5 * 60 * 1000;
|
|
4133
|
+
for (const [sessionID, timestamp] of recentErrors) {
|
|
4134
|
+
if (timestamp < cutoff) {
|
|
4135
|
+
recentErrors.delete(sessionID);
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
}, 5 * 60 * 1000);
|
|
4070
4139
|
function getNotificationTitle(config, projectName) {
|
|
4071
4140
|
if (config.showProjectName && projectName) {
|
|
4072
4141
|
return `OpenCode (${projectName})`;
|
|
@@ -4087,7 +4156,8 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds, sessi
|
|
|
4087
4156
|
}
|
|
4088
4157
|
if (isEventSoundEnabled(config, eventType)) {
|
|
4089
4158
|
const customSoundPath = getSoundPath(config, eventType);
|
|
4090
|
-
|
|
4159
|
+
const soundVolume = getSoundVolume(config, eventType);
|
|
4160
|
+
promises.push(playSound(eventType, customSoundPath, soundVolume));
|
|
4091
4161
|
}
|
|
4092
4162
|
const minDuration = config.command?.minDuration;
|
|
4093
4163
|
const shouldSkipCommand = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
|
|
@@ -4103,7 +4173,7 @@ function getSessionIDFromEvent(event) {
|
|
|
4103
4173
|
}
|
|
4104
4174
|
return null;
|
|
4105
4175
|
}
|
|
4106
|
-
async function getElapsedSinceLastPrompt(client, sessionID) {
|
|
4176
|
+
async function getElapsedSinceLastPrompt(client, sessionID, nowMs = Date.now()) {
|
|
4107
4177
|
try {
|
|
4108
4178
|
const response = await client.session.messages({ path: { id: sessionID } });
|
|
4109
4179
|
const messages = response.data ?? [];
|
|
@@ -4117,7 +4187,7 @@ async function getElapsedSinceLastPrompt(client, sessionID) {
|
|
|
4117
4187
|
}
|
|
4118
4188
|
}
|
|
4119
4189
|
if (lastUserMessageTime !== null) {
|
|
4120
|
-
return (
|
|
4190
|
+
return (nowMs - lastUserMessageTime) / 1000;
|
|
4121
4191
|
}
|
|
4122
4192
|
} catch {}
|
|
4123
4193
|
return null;
|
|
@@ -4164,11 +4234,18 @@ var NotifierPlugin = async ({ client, directory }) => {
|
|
|
4164
4234
|
if (event.type === "session.idle") {
|
|
4165
4235
|
const sessionID = getSessionIDFromEvent(event);
|
|
4166
4236
|
if (sessionID) {
|
|
4167
|
-
const
|
|
4168
|
-
if (
|
|
4169
|
-
|
|
4237
|
+
const errorTime = recentErrors.get(sessionID);
|
|
4238
|
+
if (errorTime && Date.now() - errorTime < 500) {
|
|
4239
|
+
recentErrors.delete(sessionID);
|
|
4240
|
+
const sessionInfo = await getSessionInfo(client, sessionID);
|
|
4241
|
+
await handleEventWithElapsedTime(client, config, "interrupted", projectName, event, sessionInfo.title);
|
|
4170
4242
|
} else {
|
|
4171
|
-
await
|
|
4243
|
+
const sessionInfo = await getSessionInfo(client, sessionID);
|
|
4244
|
+
if (!sessionInfo.isChild) {
|
|
4245
|
+
await handleEventWithElapsedTime(client, config, "complete", projectName, event, sessionInfo.title);
|
|
4246
|
+
} else {
|
|
4247
|
+
await handleEventWithElapsedTime(client, config, "subagent_complete", projectName, event, sessionInfo.title);
|
|
4248
|
+
}
|
|
4172
4249
|
}
|
|
4173
4250
|
} else {
|
|
4174
4251
|
await handleEventWithElapsedTime(client, config, "complete", projectName, event);
|
|
@@ -4176,12 +4253,22 @@ var NotifierPlugin = async ({ client, directory }) => {
|
|
|
4176
4253
|
}
|
|
4177
4254
|
if (event.type === "session.error") {
|
|
4178
4255
|
const sessionID = getSessionIDFromEvent(event);
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4256
|
+
if (sessionID) {
|
|
4257
|
+
recentErrors.set(sessionID, Date.now());
|
|
4258
|
+
let sessionTitle = null;
|
|
4259
|
+
if (config.showSessionTitle) {
|
|
4260
|
+
const info = await getSessionInfo(client, sessionID);
|
|
4261
|
+
sessionTitle = info.title;
|
|
4262
|
+
}
|
|
4263
|
+
setTimeout(() => {
|
|
4264
|
+
if (recentErrors.has(sessionID)) {
|
|
4265
|
+
recentErrors.delete(sessionID);
|
|
4266
|
+
handleEventWithElapsedTime(client, config, "error", projectName, event, sessionTitle);
|
|
4267
|
+
}
|
|
4268
|
+
}, 100);
|
|
4269
|
+
} else {
|
|
4270
|
+
await handleEventWithElapsedTime(client, config, "error", projectName, event);
|
|
4183
4271
|
}
|
|
4184
|
-
await handleEventWithElapsedTime(client, config, "error", projectName, event, sessionTitle);
|
|
4185
4272
|
}
|
|
4186
4273
|
},
|
|
4187
4274
|
"permission.ask": async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mohak34/opencode-notifier",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22-beta.0",
|
|
4
4
|
"description": "OpenCode plugin that sends system notifications and plays sounds when permission is needed, generation completes, or errors occur",
|
|
5
5
|
"author": "mohak34",
|
|
6
6
|
"license": "MIT",
|