@mohak34/opencode-notifier 0.2.1 → 0.2.2

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.
Files changed (3) hide show
  1. package/README.md +57 -18
  2. package/dist/index.js +159 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -50,6 +50,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
50
50
  {
51
51
  "sound": true,
52
52
  "notification": true,
53
+ "bell": false,
53
54
  "timeout": 5,
54
55
  "showProjectName": true,
55
56
  "showSessionTitle": false,
@@ -67,12 +68,16 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
67
68
  "minDuration": 0
68
69
  },
69
70
  "events": {
70
- "permission": { "sound": true, "notification": true, "command": true },
71
- "complete": { "sound": true, "notification": true, "command": true },
72
- "subagent_complete": { "sound": false, "notification": false, "command": true },
73
- "error": { "sound": true, "notification": true, "command": true },
74
- "question": { "sound": true, "notification": true, "command": true },
75
- "user_cancelled": { "sound": false, "notification": false, "command": true }
71
+ "permission": { "sound": true, "notification": true, "command": true, "bell": false },
72
+ "complete": { "sound": true, "notification": true, "command": true, "bell": false },
73
+ "subagent_complete": { "sound": false, "notification": false, "command": true, "bell": false },
74
+ "error": { "sound": true, "notification": true, "command": true, "bell": false },
75
+ "question": { "sound": true, "notification": true, "command": true, "bell": false },
76
+ "user_cancelled": { "sound": false, "notification": false, "command": true, "bell": false },
77
+ "plan_exit": { "sound": true, "notification": true, "command": true, "bell": false },
78
+ "session_started": { "sound": true, "notification": false, "command": true, "bell": false },
79
+ "user_message": { "sound": true, "notification": false, "command": true, "bell": false },
80
+ "client_connected": { "sound": true, "notification": false, "command": true, "bell": false }
76
81
  },
77
82
  "messages": {
78
83
  "permission": "Session needs permission: {sessionTitle}",
@@ -80,7 +85,11 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
80
85
  "subagent_complete": "Subagent task completed: {sessionTitle}",
81
86
  "error": "Session encountered an error: {sessionTitle}",
82
87
  "question": "Session has a question: {sessionTitle}",
83
- "user_cancelled": "Session was cancelled by user: {sessionTitle}"
88
+ "user_cancelled": "Session was cancelled by user: {sessionTitle}",
89
+ "plan_exit": "Plan ready for review: {sessionTitle}",
90
+ "session_started": "Session started: {sessionTitle}",
91
+ "user_message": "User sent a message: {sessionTitle}",
92
+ "client_connected": "OpenCode connected"
84
93
  },
85
94
  "sounds": {
86
95
  "permission": null,
@@ -88,7 +97,11 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
88
97
  "subagent_complete": null,
89
98
  "error": null,
90
99
  "question": null,
91
- "user_cancelled": null
100
+ "user_cancelled": null,
101
+ "plan_exit": null,
102
+ "session_started": null,
103
+ "user_message": null,
104
+ "client_connected": null
92
105
  },
93
106
  "volumes": {
94
107
  "permission": 1,
@@ -96,7 +109,11 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
96
109
  "subagent_complete": 1,
97
110
  "error": 1,
98
111
  "question": 1,
99
- "user_cancelled": 1
112
+ "user_cancelled": 1,
113
+ "plan_exit": 1,
114
+ "session_started": 1,
115
+ "user_message": 1,
116
+ "client_connected": 1
100
117
  }
101
118
  }
102
119
  ```
@@ -109,6 +126,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
109
126
  {
110
127
  "sound": true,
111
128
  "notification": true,
129
+ "bell": false,
112
130
  "timeout": 5,
113
131
  "showProjectName": true,
114
132
  "showSessionTitle": false,
@@ -121,6 +139,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
121
139
 
122
140
  - `sound` - Turn sounds on/off (default: true)
123
141
  - `notification` - Turn notifications on/off (default: true)
142
+ - `bell` - Emit terminal BEL (`\x07`) on events (default: false). Behavior depends on your terminal/WM settings
124
143
  - `timeout` - How long notifications show in seconds, Linux only (default: 5)
125
144
  - `showProjectName` - Show folder name in notification title (default: true)
126
145
  - `showSessionTitle` - Include the session title in notification messages via `{sessionTitle}` placeholder (default: false)
@@ -137,20 +156,28 @@ Control each event separately:
137
156
  ```json
138
157
  {
139
158
  "events": {
140
- "permission": { "sound": true, "notification": true, "command": true },
141
- "complete": { "sound": true, "notification": true, "command": true },
142
- "subagent_complete": { "sound": false, "notification": false, "command": true },
143
- "error": { "sound": true, "notification": true, "command": true },
144
- "question": { "sound": true, "notification": true, "command": true },
145
- "user_cancelled": { "sound": false, "notification": false, "command": true }
159
+ "permission": { "sound": true, "notification": true, "command": true, "bell": false },
160
+ "complete": { "sound": true, "notification": true, "command": true, "bell": false },
161
+ "subagent_complete": { "sound": false, "notification": false, "command": true, "bell": false },
162
+ "error": { "sound": true, "notification": true, "command": true, "bell": false },
163
+ "question": { "sound": true, "notification": true, "command": true, "bell": false },
164
+ "user_cancelled": { "sound": false, "notification": false, "command": true, "bell": false },
165
+ "plan_exit": { "sound": true, "notification": true, "command": true, "bell": false },
166
+ "session_started": { "sound": true, "notification": false, "command": true, "bell": false },
167
+ "user_message": { "sound": true, "notification": false, "command": true, "bell": false },
168
+ "client_connected": { "sound": true, "notification": false, "command": true, "bell": false }
146
169
  }
147
170
  }
148
171
  ```
149
172
 
150
173
  `user_cancelled` fires when you press ESC to abort a session. It's silent by default so intentional cancellations don't trigger error alerts. Set `sound` or `notification` to `true` if you want confirmation when cancelling.
151
174
 
175
+ `session_started` fires when a new top-level session is created. `user_message` fires when a user message is submitted in a top-level session. `client_connected` fires shortly after the plugin initializes and is best-effort (there is no dedicated SDK connection event from plugin context).
176
+
152
177
  The `command` property controls whether the custom command (see [Custom commands](#custom-commands)) runs for that event. Defaults to `true` for all events. Set it to `false` to suppress the command for specific events without disabling it globally.
153
178
 
179
+ `bell` is terminal-driven and may be audible, visual, both, or ignored depending on your terminal setup. Quick check: `printf '\a'`.
180
+
154
181
  Or use true/false for both:
155
182
 
156
183
  ```json
@@ -173,7 +200,11 @@ Customize the notification text:
173
200
  "subagent_complete": "Subagent task completed: {sessionTitle}",
174
201
  "error": "Session encountered an error: {sessionTitle}",
175
202
  "question": "Session has a question: {sessionTitle}",
176
- "user_cancelled": "Session was cancelled by user: {sessionTitle}"
203
+ "user_cancelled": "Session was cancelled by user: {sessionTitle}",
204
+ "plan_exit": "Plan ready for review: {sessionTitle}",
205
+ "session_started": "Session started: {sessionTitle}",
206
+ "user_message": "User sent a message: {sessionTitle}",
207
+ "client_connected": "OpenCode connected"
177
208
  }
178
209
  }
179
210
  ```
@@ -204,7 +235,11 @@ Use your own sound files:
204
235
  "subagent_complete": "/path/to/subagent-done.wav",
205
236
  "error": "/path/to/error.wav",
206
237
  "question": "/path/to/question.wav",
207
- "user_cancelled": "/path/to/cancelled.wav"
238
+ "user_cancelled": "/path/to/cancelled.wav",
239
+ "plan_exit": "/path/to/plan-ready.wav",
240
+ "session_started": "/path/to/session-started.wav",
241
+ "user_message": "/path/to/user-message.wav",
242
+ "client_connected": "/path/to/client-connected.wav"
208
243
  }
209
244
  }
210
245
  ```
@@ -226,7 +261,11 @@ Set per-event volume from `0` to `1`:
226
261
  "subagent_complete": 0.15,
227
262
  "error": 1,
228
263
  "question": 0.7,
229
- "user_cancelled": 0.5
264
+ "user_cancelled": 0.5,
265
+ "plan_exit": 0.6,
266
+ "session_started": 0.35,
267
+ "user_message": 0.2,
268
+ "client_connected": 0.45
230
269
  }
231
270
  }
232
271
  ```
package/dist/index.js CHANGED
@@ -10,11 +10,13 @@ import { fileURLToPath } from "url";
10
10
  var DEFAULT_EVENT_CONFIG = {
11
11
  sound: true,
12
12
  notification: true,
13
- command: true
13
+ command: true,
14
+ bell: false
14
15
  };
15
16
  var DEFAULT_CONFIG = {
16
17
  sound: true,
17
18
  notification: true,
19
+ bell: false,
18
20
  timeout: 5,
19
21
  showProjectName: true,
20
22
  showSessionTitle: false,
@@ -37,7 +39,11 @@ var DEFAULT_CONFIG = {
37
39
  error: { ...DEFAULT_EVENT_CONFIG },
38
40
  question: { ...DEFAULT_EVENT_CONFIG },
39
41
  interrupted: { ...DEFAULT_EVENT_CONFIG },
40
- user_cancelled: { ...DEFAULT_EVENT_CONFIG, sound: false, notification: false }
42
+ user_cancelled: { ...DEFAULT_EVENT_CONFIG, sound: false, notification: false },
43
+ plan_exit: { ...DEFAULT_EVENT_CONFIG },
44
+ session_started: { ...DEFAULT_EVENT_CONFIG, notification: false },
45
+ user_message: { ...DEFAULT_EVENT_CONFIG, notification: false },
46
+ client_connected: { ...DEFAULT_EVENT_CONFIG, notification: false }
41
47
  },
42
48
  messages: {
43
49
  permission: "Session needs permission: {sessionTitle}",
@@ -46,7 +52,11 @@ var DEFAULT_CONFIG = {
46
52
  error: "Session encountered an error: {sessionTitle}",
47
53
  question: "Session has a question: {sessionTitle}",
48
54
  interrupted: "Session was interrupted: {sessionTitle}",
49
- user_cancelled: "Session was cancelled by user: {sessionTitle}"
55
+ user_cancelled: "Session was cancelled by user: {sessionTitle}",
56
+ plan_exit: "Plan ready for review: {sessionTitle}",
57
+ session_started: "Session started: {sessionTitle}",
58
+ user_message: "User sent a message: {sessionTitle}",
59
+ client_connected: "OpenCode connected"
50
60
  },
51
61
  sounds: {
52
62
  permission: null,
@@ -55,7 +65,11 @@ var DEFAULT_CONFIG = {
55
65
  error: null,
56
66
  question: null,
57
67
  interrupted: null,
58
- user_cancelled: null
68
+ user_cancelled: null,
69
+ plan_exit: null,
70
+ session_started: null,
71
+ user_message: null,
72
+ client_connected: null
59
73
  },
60
74
  volumes: {
61
75
  permission: 1,
@@ -64,7 +78,11 @@ var DEFAULT_CONFIG = {
64
78
  error: 1,
65
79
  question: 1,
66
80
  interrupted: 1,
67
- user_cancelled: 1
81
+ user_cancelled: 1,
82
+ plan_exit: 1,
83
+ session_started: 1,
84
+ user_message: 1,
85
+ client_connected: 1
68
86
  }
69
87
  };
70
88
  function getConfigPath() {
@@ -85,13 +103,15 @@ function parseEventConfig(userEvent, defaultConfig) {
85
103
  return {
86
104
  sound: userEvent,
87
105
  notification: userEvent,
88
- command: userEvent
106
+ command: userEvent,
107
+ bell: defaultConfig.bell
89
108
  };
90
109
  }
91
110
  return {
92
111
  sound: userEvent.sound ?? defaultConfig.sound,
93
112
  notification: userEvent.notification ?? defaultConfig.notification,
94
- command: userEvent.command ?? defaultConfig.command
113
+ command: userEvent.command ?? defaultConfig.command,
114
+ bell: userEvent.bell ?? defaultConfig.bell
95
115
  };
96
116
  }
97
117
  function parseVolume(value, defaultVolume) {
@@ -116,10 +136,12 @@ function loadConfig() {
116
136
  const userConfig = JSON.parse(fileContent);
117
137
  const globalSound = userConfig.sound ?? DEFAULT_CONFIG.sound;
118
138
  const globalNotification = userConfig.notification ?? DEFAULT_CONFIG.notification;
139
+ const globalBell = userConfig.bell ?? DEFAULT_CONFIG.bell;
119
140
  const defaultWithGlobal = {
120
141
  sound: globalSound,
121
142
  notification: globalNotification,
122
- command: true
143
+ command: true,
144
+ bell: globalBell
123
145
  };
124
146
  const userCommand = userConfig.command ?? {};
125
147
  const commandArgs = Array.isArray(userCommand.args) ? userCommand.args.filter((arg) => typeof arg === "string") : undefined;
@@ -127,6 +149,7 @@ function loadConfig() {
127
149
  return {
128
150
  sound: globalSound,
129
151
  notification: globalNotification,
152
+ bell: globalBell,
130
153
  timeout: typeof userConfig.timeout === "number" && userConfig.timeout > 0 ? userConfig.timeout : DEFAULT_CONFIG.timeout,
131
154
  showProjectName: userConfig.showProjectName ?? DEFAULT_CONFIG.showProjectName,
132
155
  showSessionTitle: userConfig.showSessionTitle ?? DEFAULT_CONFIG.showSessionTitle,
@@ -146,11 +169,15 @@ function loadConfig() {
146
169
  events: {
147
170
  permission: parseEventConfig(userConfig.events?.permission ?? userConfig.permission, defaultWithGlobal),
148
171
  complete: parseEventConfig(userConfig.events?.complete ?? userConfig.complete, defaultWithGlobal),
149
- subagent_complete: parseEventConfig(userConfig.events?.subagent_complete ?? userConfig.subagent_complete, { sound: false, notification: false, command: true }),
172
+ subagent_complete: parseEventConfig(userConfig.events?.subagent_complete ?? userConfig.subagent_complete, { sound: false, notification: false, command: true, bell: false }),
150
173
  error: parseEventConfig(userConfig.events?.error ?? userConfig.error, defaultWithGlobal),
151
174
  question: parseEventConfig(userConfig.events?.question ?? userConfig.question, defaultWithGlobal),
152
175
  interrupted: parseEventConfig(userConfig.events?.interrupted ?? userConfig.interrupted, defaultWithGlobal),
153
- user_cancelled: parseEventConfig(userConfig.events?.user_cancelled ?? userConfig.user_cancelled, { sound: false, notification: false, command: true })
176
+ user_cancelled: parseEventConfig(userConfig.events?.user_cancelled ?? userConfig.user_cancelled, { sound: false, notification: false, command: true, bell: false }),
177
+ plan_exit: parseEventConfig(userConfig.events?.plan_exit ?? userConfig.plan_exit, defaultWithGlobal),
178
+ session_started: parseEventConfig(userConfig.events?.session_started ?? userConfig.session_started, { ...defaultWithGlobal, notification: false }),
179
+ user_message: parseEventConfig(userConfig.events?.user_message ?? userConfig.user_message, { ...defaultWithGlobal, notification: false }),
180
+ client_connected: parseEventConfig(userConfig.events?.client_connected ?? userConfig.client_connected, { ...defaultWithGlobal, notification: false })
154
181
  },
155
182
  messages: {
156
183
  permission: userConfig.messages?.permission ?? DEFAULT_CONFIG.messages.permission,
@@ -159,7 +186,11 @@ function loadConfig() {
159
186
  error: userConfig.messages?.error ?? DEFAULT_CONFIG.messages.error,
160
187
  question: userConfig.messages?.question ?? DEFAULT_CONFIG.messages.question,
161
188
  interrupted: userConfig.messages?.interrupted ?? DEFAULT_CONFIG.messages.interrupted,
162
- user_cancelled: userConfig.messages?.user_cancelled ?? DEFAULT_CONFIG.messages.user_cancelled
189
+ user_cancelled: userConfig.messages?.user_cancelled ?? DEFAULT_CONFIG.messages.user_cancelled,
190
+ plan_exit: userConfig.messages?.plan_exit ?? DEFAULT_CONFIG.messages.plan_exit,
191
+ session_started: userConfig.messages?.session_started ?? DEFAULT_CONFIG.messages.session_started,
192
+ user_message: userConfig.messages?.user_message ?? DEFAULT_CONFIG.messages.user_message,
193
+ client_connected: userConfig.messages?.client_connected ?? DEFAULT_CONFIG.messages.client_connected
163
194
  },
164
195
  sounds: {
165
196
  permission: userConfig.sounds?.permission ?? DEFAULT_CONFIG.sounds.permission,
@@ -168,7 +199,11 @@ function loadConfig() {
168
199
  error: userConfig.sounds?.error ?? DEFAULT_CONFIG.sounds.error,
169
200
  question: userConfig.sounds?.question ?? DEFAULT_CONFIG.sounds.question,
170
201
  interrupted: userConfig.sounds?.interrupted ?? DEFAULT_CONFIG.sounds.interrupted,
171
- user_cancelled: userConfig.sounds?.user_cancelled ?? DEFAULT_CONFIG.sounds.user_cancelled
202
+ user_cancelled: userConfig.sounds?.user_cancelled ?? DEFAULT_CONFIG.sounds.user_cancelled,
203
+ plan_exit: userConfig.sounds?.plan_exit ?? DEFAULT_CONFIG.sounds.plan_exit,
204
+ session_started: userConfig.sounds?.session_started ?? DEFAULT_CONFIG.sounds.session_started,
205
+ user_message: userConfig.sounds?.user_message ?? DEFAULT_CONFIG.sounds.user_message,
206
+ client_connected: userConfig.sounds?.client_connected ?? DEFAULT_CONFIG.sounds.client_connected
172
207
  },
173
208
  volumes: {
174
209
  permission: parseVolume(userConfig.volumes?.permission, DEFAULT_CONFIG.volumes.permission),
@@ -177,7 +212,11 @@ function loadConfig() {
177
212
  error: parseVolume(userConfig.volumes?.error, DEFAULT_CONFIG.volumes.error),
178
213
  question: parseVolume(userConfig.volumes?.question, DEFAULT_CONFIG.volumes.question),
179
214
  interrupted: parseVolume(userConfig.volumes?.interrupted, DEFAULT_CONFIG.volumes.interrupted),
180
- user_cancelled: parseVolume(userConfig.volumes?.user_cancelled, DEFAULT_CONFIG.volumes.user_cancelled)
215
+ user_cancelled: parseVolume(userConfig.volumes?.user_cancelled, DEFAULT_CONFIG.volumes.user_cancelled),
216
+ plan_exit: parseVolume(userConfig.volumes?.plan_exit, DEFAULT_CONFIG.volumes.plan_exit),
217
+ session_started: parseVolume(userConfig.volumes?.session_started, DEFAULT_CONFIG.volumes.session_started),
218
+ user_message: parseVolume(userConfig.volumes?.user_message, DEFAULT_CONFIG.volumes.user_message),
219
+ client_connected: parseVolume(userConfig.volumes?.client_connected, DEFAULT_CONFIG.volumes.client_connected)
181
220
  }
182
221
  };
183
222
  } catch {
@@ -193,6 +232,9 @@ function isEventNotificationEnabled(config, event) {
193
232
  function isEventCommandEnabled(config, event) {
194
233
  return config.events[event].command;
195
234
  }
235
+ function isEventBellEnabled(config, event) {
236
+ return config.events[event].bell;
237
+ }
196
238
  function getMessage(config, event) {
197
239
  return config.messages[event];
198
240
  }
@@ -492,6 +534,24 @@ async function playSound(event, customPath, volume) {
492
534
  } catch {}
493
535
  }
494
536
 
537
+ // src/bell.ts
538
+ var lastBellTime = 0;
539
+ var BELL_DEBOUNCE_MS = 500;
540
+ function ringBell(now = Date.now()) {
541
+ if (!process.stdout.isTTY) {
542
+ return Promise.resolve();
543
+ }
544
+ if (now - lastBellTime < BELL_DEBOUNCE_MS) {
545
+ return Promise.resolve();
546
+ }
547
+ lastBellTime = now;
548
+ return new Promise((resolve) => {
549
+ process.stdout.write("\x07", () => {
550
+ resolve();
551
+ });
552
+ });
553
+ }
554
+
495
555
  // src/command.ts
496
556
  import { spawn as spawn2 } from "child_process";
497
557
  function substituteTokens(value, event, message, sessionTitle, agentName, projectName, timestamp, turn) {
@@ -523,7 +583,7 @@ var MAC_TERMINAL_APP_NAMES = new Set([
523
583
  "terminal",
524
584
  "iterm2",
525
585
  "ghostty",
526
- "wezterm",
586
+ "wezterm-gui",
527
587
  "alacritty",
528
588
  "kitty",
529
589
  "hyper",
@@ -662,6 +722,8 @@ function getExpectedMacTerminalAppNames(env) {
662
722
  expected.add("code insiders");
663
723
  } else if (termProgram === "warpterminal") {
664
724
  expected.add("warp");
725
+ } else if (termProgram === "wezterm") {
726
+ expected.add("wezterm-gui");
665
727
  } else if (termProgram.length > 0) {
666
728
  expected.add(termProgram);
667
729
  }
@@ -786,6 +848,28 @@ var pendingIdleTimers = new Map;
786
848
  var sessionIdleSequence = new Map;
787
849
  var sessionErrorSuppressionAt = new Map;
788
850
  var sessionLastBusyAt = new Map;
851
+ var subagentSessionIds = new Set;
852
+ function asRecord(value) {
853
+ return value !== null && typeof value === "object" ? value : null;
854
+ }
855
+ function getNestedRecord(root, ...path) {
856
+ let current = root;
857
+ for (const key of path) {
858
+ const record = asRecord(current);
859
+ if (!record || !(key in record)) {
860
+ return null;
861
+ }
862
+ current = record[key];
863
+ }
864
+ return asRecord(current);
865
+ }
866
+ function getStringField(record, key) {
867
+ if (!record) {
868
+ return null;
869
+ }
870
+ const value = record[key];
871
+ return typeof value === "string" && value.length > 0 ? value : null;
872
+ }
789
873
  var globalTurnCount = null;
790
874
  function loadTurnCount() {
791
875
  try {
@@ -815,6 +899,7 @@ setInterval(() => {
815
899
  for (const [sessionID] of sessionIdleSequence) {
816
900
  if (!pendingIdleTimers.has(sessionID)) {
817
901
  sessionIdleSequence.delete(sessionID);
902
+ subagentSessionIds.delete(sessionID);
818
903
  }
819
904
  }
820
905
  for (const [sessionID, timestamp] of sessionErrorSuppressionAt) {
@@ -886,6 +971,9 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds, sessi
886
971
  const soundVolume = getSoundVolume(config, eventType);
887
972
  promises.push(playSound(eventType, customSoundPath, soundVolume));
888
973
  }
974
+ if (isEventBellEnabled(config, eventType)) {
975
+ promises.push(ringBell());
976
+ }
889
977
  const minDuration = config.command?.minDuration;
890
978
  const shouldSkipCommand = !isEventCommandEnabled(config, eventType) || typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
891
979
  if (!shouldSkipCommand) {
@@ -894,11 +982,23 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds, sessi
894
982
  await Promise.allSettled(promises);
895
983
  }
896
984
  function getSessionIDFromEvent(event) {
897
- const sessionID = event?.properties?.sessionID;
898
- if (typeof sessionID === "string" && sessionID.length > 0) {
899
- return sessionID;
900
- }
901
- return null;
985
+ const properties = getNestedRecord(event, "properties");
986
+ return getStringField(properties, "sessionID");
987
+ }
988
+ function getSessionLifecycleInfo(event) {
989
+ const info = getNestedRecord(event, "properties", "info");
990
+ return {
991
+ id: getStringField(info, "id"),
992
+ title: getStringField(info, "title"),
993
+ parentID: getStringField(info, "parentID")
994
+ };
995
+ }
996
+ function getMessageUpdatedInfo(event) {
997
+ const info = getNestedRecord(event, "properties", "info");
998
+ return {
999
+ role: getStringField(info, "role"),
1000
+ sessionID: getStringField(info, "sessionID")
1001
+ };
902
1002
  }
903
1003
  function clearPendingIdleTimer(sessionID) {
904
1004
  const timer = pendingIdleTimers.get(sessionID);
@@ -984,6 +1084,10 @@ async function processSessionIdle(client, config, projectName, event, sessionID,
984
1084
  if (shouldSuppressSessionIdle(sessionID)) {
985
1085
  return;
986
1086
  }
1087
+ if (subagentSessionIds.has(sessionID)) {
1088
+ await handleEventWithElapsedTime(client, config, "subagent_complete", projectName, event, idleReceivedAtMs, null);
1089
+ return;
1090
+ }
987
1091
  const sessionInfo = await getSessionInfo(client, sessionID);
988
1092
  if (!hasCurrentSessionIdleSequence(sessionID, sequence)) {
989
1093
  return;
@@ -995,6 +1099,7 @@ async function processSessionIdle(client, config, projectName, event, sessionID,
995
1099
  await handleEventWithElapsedTime(client, config, "complete", projectName, event, idleReceivedAtMs, sessionInfo.title);
996
1100
  return;
997
1101
  }
1102
+ subagentSessionIds.add(sessionID);
998
1103
  await handleEventWithElapsedTime(client, config, "subagent_complete", projectName, event, idleReceivedAtMs, sessionInfo.title);
999
1104
  }
1000
1105
  function scheduleSessionIdle(client, config, projectName, event, sessionID) {
@@ -1037,9 +1142,32 @@ var NotifierPlugin = async ({ client, directory }) => {
1037
1142
  }
1038
1143
  const getConfig = () => loadConfig();
1039
1144
  const projectName = directory ? basename(directory) : null;
1145
+ setTimeout(() => {
1146
+ handleEvent(getConfig(), "client_connected", projectName, null);
1147
+ }, 100);
1040
1148
  return {
1041
1149
  event: async ({ event }) => {
1042
1150
  const config = getConfig();
1151
+ if (event.type === "session.created") {
1152
+ const info = getSessionLifecycleInfo(event);
1153
+ if (info.parentID && info.id) {
1154
+ subagentSessionIds.add(info.id);
1155
+ } else {
1156
+ await handleEvent(config, "session_started", projectName, null, info.title, info.id, null);
1157
+ }
1158
+ }
1159
+ if (event.type === "session.updated") {
1160
+ const info = getSessionLifecycleInfo(event);
1161
+ if (info.parentID && info.id) {
1162
+ subagentSessionIds.add(info.id);
1163
+ }
1164
+ }
1165
+ if (event.type === "session.deleted") {
1166
+ const info = getSessionLifecycleInfo(event);
1167
+ if (info.id) {
1168
+ subagentSessionIds.delete(info.id);
1169
+ }
1170
+ }
1043
1171
  if (event.type === "permission.asked") {
1044
1172
  const sessionID = getSessionIDFromEvent(event);
1045
1173
  if (!shouldSuppressPermissionAlert(sessionID)) {
@@ -1068,6 +1196,15 @@ var NotifierPlugin = async ({ client, directory }) => {
1068
1196
  }
1069
1197
  await handleEventWithElapsedTime(client, config, eventType, projectName, event, undefined, sessionTitle);
1070
1198
  }
1199
+ if (event.type === "message.updated") {
1200
+ const info = getMessageUpdatedInfo(event);
1201
+ if (info.role === "user") {
1202
+ const sessionID = info.sessionID;
1203
+ if (!sessionID || !subagentSessionIds.has(sessionID)) {
1204
+ await handleEvent(config, "user_message", projectName, null, null, sessionID, null);
1205
+ }
1206
+ }
1207
+ }
1071
1208
  },
1072
1209
  "permission.ask": async () => {
1073
1210
  const config = getConfig();
@@ -1080,6 +1217,9 @@ var NotifierPlugin = async ({ client, directory }) => {
1080
1217
  if (input.tool === "question") {
1081
1218
  await handleEvent(config, "question", projectName, null);
1082
1219
  }
1220
+ if (input.tool === "plan_exit") {
1221
+ await handleEvent(config, "plan_exit", projectName, null);
1222
+ }
1083
1223
  }
1084
1224
  };
1085
1225
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohak34/opencode-notifier",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",