@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 CHANGED
@@ -18,7 +18,7 @@ To pin a specific version:
18
18
 
19
19
  ```json
20
20
  {
21
- "plugin": ["@mohak34/opencode-notifier@0.1.10"]
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.10`)
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.10"]
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. **Install terminal-notifier via Homebrew:**
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 your terminal app (e.g., Ghostty, iTerm2, Terminal)
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
- var NotifierPlugin = async ({ project, client, $, directory, worktree }) => {
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 handleEvent(config, "permission", projectName);
4100
+ await handleEventWithElapsedTime(client, config, "permission", projectName, event);
4010
4101
  }
4011
4102
  if (event.type === "permission.asked") {
4012
- await handleEvent(config, "permission", projectName);
4103
+ await handleEventWithElapsedTime(client, config, "permission", projectName, event);
4013
4104
  }
4014
4105
  if (event.type === "session.idle") {
4015
- await handleEvent(config, "complete", projectName);
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 handleEvent(config, "error", projectName);
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, output) => {
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.13",
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