@mohak34/opencode-notifier 0.1.13 → 0.1.15
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 +65 -12
- package/dist/index.js +111 -10
- package/package.json +1 -1
- package/sounds/subagent_complete.wav +0 -0
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ To pin a specific version:
|
|
|
18
18
|
|
|
19
19
|
```json
|
|
20
20
|
{
|
|
21
|
-
"plugin": ["@mohak34/opencode-notifier@0.1.
|
|
21
|
+
"plugin": ["@mohak34/opencode-notifier@0.1.14"]
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
@@ -46,13 +46,13 @@ Remove-Item -Recurse -Force "$env:USERPROFILE\.cache\opencode\node_modules\@moha
|
|
|
46
46
|
|
|
47
47
|
Then restart OpenCode - it will download the latest version automatically.
|
|
48
48
|
|
|
49
|
-
### If you use a pinned version (e.g., `@0.1.
|
|
49
|
+
### If you use a pinned version (e.g., `@0.1.14`)
|
|
50
50
|
|
|
51
51
|
1. Update the version in your `opencode.json`:
|
|
52
52
|
|
|
53
53
|
```json
|
|
54
54
|
{
|
|
55
|
-
"plugin": ["@mohak34/opencode-notifier@0.1.
|
|
55
|
+
"plugin": ["@mohak34/opencode-notifier@0.1.14"]
|
|
56
56
|
}
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -92,21 +92,30 @@ To customize the plugin, create `~/.config/opencode/opencode-notifier.json`:
|
|
|
92
92
|
"notification": true,
|
|
93
93
|
"timeout": 5,
|
|
94
94
|
"showProjectName": true,
|
|
95
|
+
"command": {
|
|
96
|
+
"enabled": false,
|
|
97
|
+
"path": "/path/to/command",
|
|
98
|
+
"args": ["--event", "{event}", "--message", "{message}"],
|
|
99
|
+
"minDuration": 0
|
|
100
|
+
},
|
|
95
101
|
"events": {
|
|
96
102
|
"permission": { "sound": true, "notification": true },
|
|
97
103
|
"complete": { "sound": true, "notification": true },
|
|
104
|
+
"subagent_complete": { "sound": false, "notification": false },
|
|
98
105
|
"error": { "sound": true, "notification": true },
|
|
99
106
|
"question": { "sound": true, "notification": true }
|
|
100
107
|
},
|
|
101
108
|
"messages": {
|
|
102
109
|
"permission": "Session needs permission",
|
|
103
110
|
"complete": "Session has finished",
|
|
111
|
+
"subagent_complete": "Subagent task completed",
|
|
104
112
|
"error": "Session encountered an error",
|
|
105
113
|
"question": "Session has a question"
|
|
106
114
|
},
|
|
107
115
|
"sounds": {
|
|
108
116
|
"permission": "/path/to/custom/sound.wav",
|
|
109
117
|
"complete": "/path/to/custom/sound.wav",
|
|
118
|
+
"subagent_complete": "/path/to/custom/sound.wav",
|
|
110
119
|
"error": "/path/to/custom/sound.wav",
|
|
111
120
|
"question": "/path/to/custom/sound.wav"
|
|
112
121
|
}
|
|
@@ -121,6 +130,7 @@ To customize the plugin, create `~/.config/opencode/opencode-notifier.json`:
|
|
|
121
130
|
| `notification` | boolean | `true` | Global toggle for all notifications |
|
|
122
131
|
| `timeout` | number | `5` | Notification duration in seconds (Linux only) |
|
|
123
132
|
| `showProjectName` | boolean | `true` | Show project folder name in notification title |
|
|
133
|
+
| `command` | object | — | Command execution settings (enabled/path/args/minDuration) |
|
|
124
134
|
|
|
125
135
|
### Events
|
|
126
136
|
|
|
@@ -131,6 +141,7 @@ Control sound and notification separately for each event:
|
|
|
131
141
|
"events": {
|
|
132
142
|
"permission": { "sound": true, "notification": true },
|
|
133
143
|
"complete": { "sound": false, "notification": true },
|
|
144
|
+
"subagent_complete": { "sound": true, "notification": false },
|
|
134
145
|
"error": { "sound": true, "notification": false },
|
|
135
146
|
"question": { "sound": true, "notification": true }
|
|
136
147
|
}
|
|
@@ -144,12 +155,15 @@ Or use a boolean to toggle both:
|
|
|
144
155
|
"events": {
|
|
145
156
|
"permission": true,
|
|
146
157
|
"complete": false,
|
|
158
|
+
"subagent_complete": true,
|
|
147
159
|
"error": true,
|
|
148
160
|
"question": true
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
```
|
|
152
164
|
|
|
165
|
+
Note: `complete` fires for primary (main) session completion, while `subagent_complete` fires for subagent completion. `subagent_complete` defaults to disabled (both sound and notification are false).
|
|
166
|
+
|
|
153
167
|
### Messages
|
|
154
168
|
|
|
155
169
|
Customize notification text:
|
|
@@ -159,12 +173,33 @@ Customize notification text:
|
|
|
159
173
|
"messages": {
|
|
160
174
|
"permission": "Action required",
|
|
161
175
|
"complete": "Done!",
|
|
176
|
+
"subagent_complete": "Subagent finished",
|
|
162
177
|
"error": "Something went wrong",
|
|
163
178
|
"question": "Input needed"
|
|
164
179
|
}
|
|
165
180
|
}
|
|
166
181
|
```
|
|
167
182
|
|
|
183
|
+
### Command
|
|
184
|
+
|
|
185
|
+
Run a custom command when events fire. Use `{event}` and `{message}` tokens in `path` or `args` to inject the event name and message.
|
|
186
|
+
|
|
187
|
+
`command.minDuration` (optional, seconds) gates command execution based on the time elapsed since the last user prompt.
|
|
188
|
+
- Applies to all events for the custom command.
|
|
189
|
+
- If elapsed time is known and is below `minDuration`, the command is skipped.
|
|
190
|
+
- If elapsed time cannot be determined for an event, the command still runs.
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"command": {
|
|
195
|
+
"enabled": true,
|
|
196
|
+
"path": "/path/to/command",
|
|
197
|
+
"args": ["--event", "{event}", "--message", "{message}"],
|
|
198
|
+
"minDuration": 10
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
168
203
|
### Custom Sounds
|
|
169
204
|
|
|
170
205
|
Use your own sound files:
|
|
@@ -174,6 +209,7 @@ Use your own sound files:
|
|
|
174
209
|
"sounds": {
|
|
175
210
|
"permission": "/home/user/sounds/alert.wav",
|
|
176
211
|
"complete": "/home/user/sounds/done.wav",
|
|
212
|
+
"subagent_complete": "/home/user/sounds/subagent-done.wav",
|
|
177
213
|
"error": "/home/user/sounds/error.wav",
|
|
178
214
|
"question": "/home/user/sounds/question.wav"
|
|
179
215
|
}
|
|
@@ -182,6 +218,30 @@ Use your own sound files:
|
|
|
182
218
|
|
|
183
219
|
If a custom sound file path is provided but the file doesn't exist, the plugin will fall back to the bundled sound.
|
|
184
220
|
|
|
221
|
+
### Example: Different behaviors for main and subagent completion
|
|
222
|
+
|
|
223
|
+
You may want different notification behaviors for primary sessions versus subagent sessions. For example:
|
|
224
|
+
|
|
225
|
+
- **Main session completion**: Play a sound and show a system notification
|
|
226
|
+
- **Subagent completion**: Play a different sound, but no system notification
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"events": {
|
|
231
|
+
"complete": { "sound": true, "notification": true },
|
|
232
|
+
"subagent_complete": { "sound": true, "notification": false }
|
|
233
|
+
},
|
|
234
|
+
"messages": {
|
|
235
|
+
"complete": "Session has finished",
|
|
236
|
+
"subagent_complete": "Subagent task completed"
|
|
237
|
+
},
|
|
238
|
+
"sounds": {
|
|
239
|
+
"complete": "/home/user/sounds/main-done.wav",
|
|
240
|
+
"subagent_complete": "/home/user/sounds/subagent-chime.wav"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
185
245
|
## Troubleshooting
|
|
186
246
|
|
|
187
247
|
### macOS: Notifications not showing (only sound works)
|
|
@@ -190,17 +250,10 @@ If a custom sound file path is provided but the file doesn't exist, the plugin w
|
|
|
190
250
|
|
|
191
251
|
If notifications still don't work after updating:
|
|
192
252
|
|
|
193
|
-
1. **
|
|
194
|
-
|
|
195
|
-
```bash
|
|
196
|
-
brew install terminal-notifier
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
2. **Check notification permissions:**
|
|
253
|
+
1. **Check notification permissions:**
|
|
200
254
|
- Open **System Settings > Notifications**
|
|
201
|
-
- Find
|
|
255
|
+
- Find **Script Editor** in the list
|
|
202
256
|
- Make sure notifications are set to **Banners** or **Alerts**
|
|
203
|
-
- Also enable notifications for **terminal-notifier** if it appears in the list
|
|
204
257
|
|
|
205
258
|
### Linux: Notifications not showing
|
|
206
259
|
|
package/dist/index.js
CHANGED
|
@@ -3742,21 +3742,29 @@ var DEFAULT_CONFIG = {
|
|
|
3742
3742
|
notification: true,
|
|
3743
3743
|
timeout: 5,
|
|
3744
3744
|
showProjectName: true,
|
|
3745
|
+
command: {
|
|
3746
|
+
enabled: false,
|
|
3747
|
+
path: "",
|
|
3748
|
+
minDuration: 0
|
|
3749
|
+
},
|
|
3745
3750
|
events: {
|
|
3746
3751
|
permission: { ...DEFAULT_EVENT_CONFIG },
|
|
3747
3752
|
complete: { ...DEFAULT_EVENT_CONFIG },
|
|
3753
|
+
subagent_complete: { sound: false, notification: false },
|
|
3748
3754
|
error: { ...DEFAULT_EVENT_CONFIG },
|
|
3749
3755
|
question: { ...DEFAULT_EVENT_CONFIG }
|
|
3750
3756
|
},
|
|
3751
3757
|
messages: {
|
|
3752
3758
|
permission: "Session needs permission",
|
|
3753
3759
|
complete: "Session has finished",
|
|
3760
|
+
subagent_complete: "Subagent task completed",
|
|
3754
3761
|
error: "Session encountered an error",
|
|
3755
3762
|
question: "Session has a question"
|
|
3756
3763
|
},
|
|
3757
3764
|
sounds: {
|
|
3758
3765
|
permission: null,
|
|
3759
3766
|
complete: null,
|
|
3767
|
+
subagent_complete: null,
|
|
3760
3768
|
error: null,
|
|
3761
3769
|
question: null
|
|
3762
3770
|
}
|
|
@@ -3793,26 +3801,38 @@ function loadConfig() {
|
|
|
3793
3801
|
sound: globalSound,
|
|
3794
3802
|
notification: globalNotification
|
|
3795
3803
|
};
|
|
3804
|
+
const userCommand = userConfig.command ?? {};
|
|
3805
|
+
const commandArgs = Array.isArray(userCommand.args) ? userCommand.args.filter((arg) => typeof arg === "string") : undefined;
|
|
3806
|
+
const commandMinDuration = typeof userCommand.minDuration === "number" && Number.isFinite(userCommand.minDuration) && userCommand.minDuration > 0 ? userCommand.minDuration : 0;
|
|
3796
3807
|
return {
|
|
3797
3808
|
sound: globalSound,
|
|
3798
3809
|
notification: globalNotification,
|
|
3799
3810
|
timeout: typeof userConfig.timeout === "number" && userConfig.timeout > 0 ? userConfig.timeout : DEFAULT_CONFIG.timeout,
|
|
3800
3811
|
showProjectName: userConfig.showProjectName ?? DEFAULT_CONFIG.showProjectName,
|
|
3812
|
+
command: {
|
|
3813
|
+
enabled: typeof userCommand.enabled === "boolean" ? userCommand.enabled : DEFAULT_CONFIG.command.enabled,
|
|
3814
|
+
path: typeof userCommand.path === "string" ? userCommand.path : DEFAULT_CONFIG.command.path,
|
|
3815
|
+
args: commandArgs,
|
|
3816
|
+
minDuration: commandMinDuration
|
|
3817
|
+
},
|
|
3801
3818
|
events: {
|
|
3802
3819
|
permission: parseEventConfig(userConfig.events?.permission ?? userConfig.permission, defaultWithGlobal),
|
|
3803
3820
|
complete: parseEventConfig(userConfig.events?.complete ?? userConfig.complete, defaultWithGlobal),
|
|
3821
|
+
subagent_complete: parseEventConfig(userConfig.events?.subagent_complete ?? userConfig.subagent_complete, { sound: false, notification: false }),
|
|
3804
3822
|
error: parseEventConfig(userConfig.events?.error ?? userConfig.error, defaultWithGlobal),
|
|
3805
3823
|
question: parseEventConfig(userConfig.events?.question ?? userConfig.question, defaultWithGlobal)
|
|
3806
3824
|
},
|
|
3807
3825
|
messages: {
|
|
3808
3826
|
permission: userConfig.messages?.permission ?? DEFAULT_CONFIG.messages.permission,
|
|
3809
3827
|
complete: userConfig.messages?.complete ?? DEFAULT_CONFIG.messages.complete,
|
|
3828
|
+
subagent_complete: userConfig.messages?.subagent_complete ?? DEFAULT_CONFIG.messages.subagent_complete,
|
|
3810
3829
|
error: userConfig.messages?.error ?? DEFAULT_CONFIG.messages.error,
|
|
3811
3830
|
question: userConfig.messages?.question ?? DEFAULT_CONFIG.messages.question
|
|
3812
3831
|
},
|
|
3813
3832
|
sounds: {
|
|
3814
3833
|
permission: userConfig.sounds?.permission ?? DEFAULT_CONFIG.sounds.permission,
|
|
3815
3834
|
complete: userConfig.sounds?.complete ?? DEFAULT_CONFIG.sounds.complete,
|
|
3835
|
+
subagent_complete: userConfig.sounds?.subagent_complete ?? DEFAULT_CONFIG.sounds.subagent_complete,
|
|
3816
3836
|
error: userConfig.sounds?.error ?? DEFAULT_CONFIG.sounds.error,
|
|
3817
3837
|
question: userConfig.sounds?.question ?? DEFAULT_CONFIG.sounds.question
|
|
3818
3838
|
}
|
|
@@ -3980,6 +4000,25 @@ async function playSound(event, customPath) {
|
|
|
3980
4000
|
} catch {}
|
|
3981
4001
|
}
|
|
3982
4002
|
|
|
4003
|
+
// src/command.ts
|
|
4004
|
+
import { spawn as spawn2 } from "child_process";
|
|
4005
|
+
function substituteTokens(value, event, message) {
|
|
4006
|
+
return value.replaceAll("{event}", event).replaceAll("{message}", message);
|
|
4007
|
+
}
|
|
4008
|
+
function runCommand2(config, event, message) {
|
|
4009
|
+
if (!config.command.enabled || !config.command.path) {
|
|
4010
|
+
return;
|
|
4011
|
+
}
|
|
4012
|
+
const args = (config.command.args ?? []).map((arg) => substituteTokens(arg, event, message));
|
|
4013
|
+
const command = substituteTokens(config.command.path, event, message);
|
|
4014
|
+
const proc = spawn2(command, args, {
|
|
4015
|
+
stdio: "ignore",
|
|
4016
|
+
detached: true
|
|
4017
|
+
});
|
|
4018
|
+
proc.on("error", () => {});
|
|
4019
|
+
proc.unref();
|
|
4020
|
+
}
|
|
4021
|
+
|
|
3983
4022
|
// src/index.ts
|
|
3984
4023
|
function getNotificationTitle(config, projectName) {
|
|
3985
4024
|
if (config.showProjectName && projectName) {
|
|
@@ -3987,43 +4026,105 @@ function getNotificationTitle(config, projectName) {
|
|
|
3987
4026
|
}
|
|
3988
4027
|
return "OpenCode";
|
|
3989
4028
|
}
|
|
3990
|
-
async function handleEvent(config, eventType, projectName) {
|
|
4029
|
+
async function handleEvent(config, eventType, projectName, elapsedSeconds) {
|
|
3991
4030
|
const promises = [];
|
|
4031
|
+
const message = getMessage(config, eventType);
|
|
3992
4032
|
if (isEventNotificationEnabled(config, eventType)) {
|
|
3993
4033
|
const title = getNotificationTitle(config, projectName);
|
|
3994
|
-
const message = getMessage(config, eventType);
|
|
3995
4034
|
promises.push(sendNotification(title, message, config.timeout));
|
|
3996
4035
|
}
|
|
3997
4036
|
if (isEventSoundEnabled(config, eventType)) {
|
|
3998
4037
|
const customSoundPath = getSoundPath(config, eventType);
|
|
3999
4038
|
promises.push(playSound(eventType, customSoundPath));
|
|
4000
4039
|
}
|
|
4040
|
+
const minDuration = config.command?.minDuration;
|
|
4041
|
+
const shouldSkipCommand = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
|
|
4042
|
+
if (!shouldSkipCommand) {
|
|
4043
|
+
runCommand2(config, eventType, message);
|
|
4044
|
+
}
|
|
4001
4045
|
await Promise.allSettled(promises);
|
|
4002
4046
|
}
|
|
4003
|
-
|
|
4047
|
+
function getSessionIDFromEvent(event) {
|
|
4048
|
+
const sessionID = event?.properties?.sessionID;
|
|
4049
|
+
if (typeof sessionID === "string" && sessionID.length > 0) {
|
|
4050
|
+
return sessionID;
|
|
4051
|
+
}
|
|
4052
|
+
return null;
|
|
4053
|
+
}
|
|
4054
|
+
async function getElapsedSinceLastPrompt(client, sessionID) {
|
|
4055
|
+
try {
|
|
4056
|
+
const response = await client.session.messages({ path: { id: sessionID } });
|
|
4057
|
+
const messages = response.data ?? [];
|
|
4058
|
+
let lastUserMessageTime = null;
|
|
4059
|
+
for (const msg of messages) {
|
|
4060
|
+
const info = msg.info;
|
|
4061
|
+
if (info.role === "user" && typeof info.time?.created === "number") {
|
|
4062
|
+
if (lastUserMessageTime === null || info.time.created > lastUserMessageTime) {
|
|
4063
|
+
lastUserMessageTime = info.time.created;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
if (lastUserMessageTime !== null) {
|
|
4068
|
+
return (Date.now() - lastUserMessageTime) / 1000;
|
|
4069
|
+
}
|
|
4070
|
+
} catch {}
|
|
4071
|
+
return null;
|
|
4072
|
+
}
|
|
4073
|
+
async function isChildSession(client, sessionID) {
|
|
4074
|
+
try {
|
|
4075
|
+
const response = await client.session.get({ path: { id: sessionID } });
|
|
4076
|
+
const parentID = response.data?.parentID;
|
|
4077
|
+
return !!parentID;
|
|
4078
|
+
} catch {
|
|
4079
|
+
return false;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
async function handleEventWithElapsedTime(client, config, eventType, projectName, event) {
|
|
4083
|
+
const minDuration = config.command?.minDuration;
|
|
4084
|
+
const shouldLookupElapsed = !!config.command?.enabled && typeof config.command?.path === "string" && config.command.path.length > 0 && typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0;
|
|
4085
|
+
let elapsedSeconds = null;
|
|
4086
|
+
if (shouldLookupElapsed) {
|
|
4087
|
+
const sessionID = getSessionIDFromEvent(event);
|
|
4088
|
+
if (sessionID) {
|
|
4089
|
+
elapsedSeconds = await getElapsedSinceLastPrompt(client, sessionID);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
await handleEvent(config, eventType, projectName, elapsedSeconds);
|
|
4093
|
+
}
|
|
4094
|
+
var NotifierPlugin = async ({ client, directory }) => {
|
|
4004
4095
|
const config = loadConfig();
|
|
4005
4096
|
const projectName = directory ? basename(directory) : null;
|
|
4006
4097
|
return {
|
|
4007
4098
|
event: async ({ event }) => {
|
|
4008
4099
|
if (event.type === "permission.updated") {
|
|
4009
|
-
await
|
|
4100
|
+
await handleEventWithElapsedTime(client, config, "permission", projectName, event);
|
|
4010
4101
|
}
|
|
4011
4102
|
if (event.type === "permission.asked") {
|
|
4012
|
-
await
|
|
4103
|
+
await handleEventWithElapsedTime(client, config, "permission", projectName, event);
|
|
4013
4104
|
}
|
|
4014
4105
|
if (event.type === "session.idle") {
|
|
4015
|
-
|
|
4106
|
+
const sessionID = getSessionIDFromEvent(event);
|
|
4107
|
+
if (sessionID) {
|
|
4108
|
+
const isChild = await isChildSession(client, sessionID);
|
|
4109
|
+
if (!isChild) {
|
|
4110
|
+
await handleEventWithElapsedTime(client, config, "complete", projectName, event);
|
|
4111
|
+
} else {
|
|
4112
|
+
await handleEventWithElapsedTime(client, config, "subagent_complete", projectName, event);
|
|
4113
|
+
}
|
|
4114
|
+
} else {
|
|
4115
|
+
await handleEventWithElapsedTime(client, config, "complete", projectName, event);
|
|
4116
|
+
}
|
|
4016
4117
|
}
|
|
4017
4118
|
if (event.type === "session.error") {
|
|
4018
|
-
await
|
|
4119
|
+
await handleEventWithElapsedTime(client, config, "error", projectName, event);
|
|
4019
4120
|
}
|
|
4020
4121
|
},
|
|
4021
4122
|
"permission.ask": async () => {
|
|
4022
|
-
await handleEvent(config, "permission", projectName);
|
|
4123
|
+
await handleEvent(config, "permission", projectName, null);
|
|
4023
4124
|
},
|
|
4024
|
-
"tool.execute.before": async (input
|
|
4125
|
+
"tool.execute.before": async (input) => {
|
|
4025
4126
|
if (input.tool === "question") {
|
|
4026
|
-
await handleEvent(config, "question", projectName);
|
|
4127
|
+
await handleEvent(config, "question", projectName, null);
|
|
4027
4128
|
}
|
|
4028
4129
|
}
|
|
4029
4130
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mohak34/opencode-notifier",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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",
|
|
Binary file
|