@mohak34/opencode-notifier 0.1.20 → 0.1.21

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 +24 -12
  2. package/dist/index.js +61 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -52,6 +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": false,
55
56
  "showIcon": true,
56
57
  "notificationSystem": "osascript",
57
58
  "command": {
@@ -68,11 +69,11 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
68
69
  "question": { "sound": true, "notification": true }
69
70
  },
70
71
  "messages": {
71
- "permission": "Session needs permission",
72
- "complete": "Session has finished",
73
- "subagent_complete": "Subagent task completed",
74
- "error": "Session encountered an error",
75
- "question": "Session has a question"
72
+ "permission": "Session needs permission: {sessionTitle}",
73
+ "complete": "Session has finished: {sessionTitle}",
74
+ "subagent_complete": "Subagent task completed: {sessionTitle}",
75
+ "error": "Session encountered an error: {sessionTitle}",
76
+ "question": "Session has a question: {sessionTitle}"
76
77
  },
77
78
  "sounds": {
78
79
  "permission": null,
@@ -101,6 +102,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
101
102
  "notification": true,
102
103
  "timeout": 5,
103
104
  "showProjectName": true,
105
+ "showSessionTitle": false,
104
106
  "showIcon": true,
105
107
  "notificationSystem": "osascript"
106
108
  }
@@ -110,6 +112,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
110
112
  - `notification` - Turn notifications on/off (default: true)
111
113
  - `timeout` - How long notifications show in seconds, Linux only (default: 5)
112
114
  - `showProjectName` - Show folder name in notification title (default: true)
115
+ - `showSessionTitle` - Include the session title in notification messages via `{sessionTitle}` placeholder (default: true)
113
116
  - `showIcon` - Show OpenCode icon, Windows/Linux only (default: true)
114
117
  - `notificationSystem` - macOS only: `"osascript"` or `"node-notifier"` (default: "osascript")
115
118
 
@@ -146,15 +149,24 @@ Customize the notification text:
146
149
  ```json
147
150
  {
148
151
  "messages": {
149
- "permission": "Session needs permission",
150
- "complete": "Session has finished",
151
- "subagent_complete": "Subagent task completed",
152
- "error": "Session encountered an error",
153
- "question": "Session has a question"
152
+ "permission": "Session needs permission: {sessionTitle}",
153
+ "complete": "Session has finished: {sessionTitle}",
154
+ "subagent_complete": "Subagent task completed: {sessionTitle}",
155
+ "error": "Session encountered an error: {sessionTitle}",
156
+ "question": "Session has a question: {sessionTitle}"
154
157
  }
155
158
  }
156
159
  ```
157
160
 
161
+ Messages support placeholder tokens that get replaced with actual values:
162
+
163
+ - `{sessionTitle}` - The title/summary of the current session (e.g. "Fix login bug")
164
+ - `{projectName}` - The project folder name
165
+
166
+ When `showSessionTitle` is `false`, `{sessionTitle}` is replaced with an empty string. Any trailing separators (`: `, ` - `, ` | `) are automatically cleaned up when a placeholder resolves to empty.
167
+
168
+ To disable session titles in messages without changing `showSessionTitle`, just remove the `{sessionTitle}` placeholder from your custom messages.
169
+
158
170
  ### Sounds
159
171
 
160
172
  Use your own sound files:
@@ -198,7 +210,7 @@ Set per-event volume from `0` to `1`:
198
210
 
199
211
  ### Custom commands
200
212
 
201
- Run your own script when something happens. Use `{event}` and `{message}` as placeholders:
213
+ Run your own script when something happens. Use `{event}`, `{message}`, and `{sessionTitle}` as placeholders:
202
214
 
203
215
  ```json
204
216
  {
@@ -213,7 +225,7 @@ Run your own script when something happens. Use `{event}` and `{message}` as pla
213
225
 
214
226
  - `enabled` - Turn command on/off
215
227
  - `path` - Path to your script/executable
216
- - `args` - Arguments to pass, can use `{event}` and `{message}` tokens
228
+ - `args` - Arguments to pass, can use `{event}`, `{message}`, and `{sessionTitle}` tokens
217
229
  - `minDuration` - Skip if response was quick, avoids spam (seconds)
218
230
 
219
231
  #### Example: Log events to a file
package/dist/index.js CHANGED
@@ -3743,6 +3743,7 @@ var DEFAULT_CONFIG = {
3743
3743
  notification: true,
3744
3744
  timeout: 5,
3745
3745
  showProjectName: true,
3746
+ showSessionTitle: false,
3746
3747
  showIcon: true,
3747
3748
  notificationSystem: "osascript",
3748
3749
  command: {
@@ -3758,11 +3759,11 @@ var DEFAULT_CONFIG = {
3758
3759
  question: { ...DEFAULT_EVENT_CONFIG }
3759
3760
  },
3760
3761
  messages: {
3761
- permission: "Session needs permission",
3762
- complete: "Session has finished",
3763
- subagent_complete: "Subagent task completed",
3764
- error: "Session encountered an error",
3765
- question: "Session has a question"
3762
+ permission: "Session needs permission: {sessionTitle}",
3763
+ complete: "Session has finished: {sessionTitle}",
3764
+ subagent_complete: "Subagent task completed: {sessionTitle}",
3765
+ error: "Session encountered an error: {sessionTitle}",
3766
+ question: "Session has a question: {sessionTitle}"
3766
3767
  },
3767
3768
  sounds: {
3768
3769
  permission: null,
@@ -3831,6 +3832,7 @@ function loadConfig() {
3831
3832
  notification: globalNotification,
3832
3833
  timeout: typeof userConfig.timeout === "number" && userConfig.timeout > 0 ? userConfig.timeout : DEFAULT_CONFIG.timeout,
3833
3834
  showProjectName: userConfig.showProjectName ?? DEFAULT_CONFIG.showProjectName,
3835
+ showSessionTitle: userConfig.showSessionTitle ?? DEFAULT_CONFIG.showSessionTitle,
3834
3836
  showIcon: userConfig.showIcon ?? DEFAULT_CONFIG.showIcon,
3835
3837
  notificationSystem: userConfig.notificationSystem === "node-notifier" ? "node-notifier" : "osascript",
3836
3838
  command: {
@@ -3901,6 +3903,16 @@ function getIconPath(config) {
3901
3903
  } catch {}
3902
3904
  return;
3903
3905
  }
3906
+ function interpolateMessage(message, context) {
3907
+ let result = message;
3908
+ const sessionTitle = context.sessionTitle || "";
3909
+ result = result.replaceAll("{sessionTitle}", sessionTitle);
3910
+ const projectName = context.projectName || "";
3911
+ result = result.replaceAll("{projectName}", projectName);
3912
+ result = result.replace(/\s*[:\-|]\s*$/, "").trim();
3913
+ result = result.replace(/\s{2,}/g, " ");
3914
+ return result;
3915
+ }
3904
3916
 
3905
3917
  // src/notify.ts
3906
3918
  var import_node_notifier = __toESM(require_node_notifier(), 1);
@@ -4086,15 +4098,18 @@ async function playSound(event, customPath, volume) {
4086
4098
 
4087
4099
  // src/command.ts
4088
4100
  import { spawn as spawn2 } from "child_process";
4089
- function substituteTokens(value, event, message) {
4090
- return value.replaceAll("{event}", event).replaceAll("{message}", message);
4101
+ function substituteTokens(value, event, message, sessionTitle, projectName) {
4102
+ let result = value.replaceAll("{event}", event).replaceAll("{message}", message);
4103
+ result = result.replaceAll("{sessionTitle}", sessionTitle || "");
4104
+ result = result.replaceAll("{projectName}", projectName || "");
4105
+ return result;
4091
4106
  }
4092
- function runCommand2(config, event, message) {
4107
+ function runCommand2(config, event, message, sessionTitle, projectName) {
4093
4108
  if (!config.command.enabled || !config.command.path) {
4094
4109
  return;
4095
4110
  }
4096
- const args = (config.command.args ?? []).map((arg) => substituteTokens(arg, event, message));
4097
- const command = substituteTokens(config.command.path, event, message);
4111
+ const args = (config.command.args ?? []).map((arg) => substituteTokens(arg, event, message, sessionTitle, projectName));
4112
+ const command = substituteTokens(config.command.path, event, message, sessionTitle, projectName);
4098
4113
  const proc = spawn2(command, args, {
4099
4114
  stdio: "ignore",
4100
4115
  detached: true
@@ -4110,9 +4125,13 @@ function getNotificationTitle(config, projectName) {
4110
4125
  }
4111
4126
  return "OpenCode";
4112
4127
  }
4113
- async function handleEvent(config, eventType, projectName, elapsedSeconds) {
4128
+ async function handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle) {
4114
4129
  const promises = [];
4115
- const message = getMessage(config, eventType);
4130
+ const rawMessage = getMessage(config, eventType);
4131
+ const message = interpolateMessage(rawMessage, {
4132
+ sessionTitle: config.showSessionTitle ? sessionTitle : null,
4133
+ projectName
4134
+ });
4116
4135
  if (isEventNotificationEnabled(config, eventType)) {
4117
4136
  const title = getNotificationTitle(config, projectName);
4118
4137
  const iconPath = getIconPath(config);
@@ -4126,7 +4145,7 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds) {
4126
4145
  const minDuration = config.command?.minDuration;
4127
4146
  const shouldSkipCommand = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
4128
4147
  if (!shouldSkipCommand) {
4129
- runCommand2(config, eventType, message);
4148
+ runCommand2(config, eventType, message, sessionTitle, projectName);
4130
4149
  }
4131
4150
  await Promise.allSettled(promises);
4132
4151
  }
@@ -4156,26 +4175,33 @@ async function getElapsedSinceLastPrompt(client, sessionID) {
4156
4175
  } catch {}
4157
4176
  return null;
4158
4177
  }
4159
- async function isChildSession(client, sessionID) {
4178
+ async function getSessionInfo(client, sessionID) {
4160
4179
  try {
4161
4180
  const response = await client.session.get({ path: { id: sessionID } });
4162
- const parentID = response.data?.parentID;
4163
- return !!parentID;
4181
+ return {
4182
+ isChild: !!response.data?.parentID,
4183
+ title: response.data?.title ?? null
4184
+ };
4164
4185
  } catch {
4165
- return false;
4186
+ return { isChild: false, title: null };
4166
4187
  }
4167
4188
  }
4168
- async function handleEventWithElapsedTime(client, config, eventType, projectName, event) {
4169
- const minDuration = config.command?.minDuration;
4170
- const shouldLookupElapsed = !!config.command?.enabled && typeof config.command?.path === "string" && config.command.path.length > 0 && typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0;
4189
+ async function handleEventWithElapsedTime(client, config, eventType, projectName, event, preloadedSessionTitle) {
4190
+ const sessionID = getSessionIDFromEvent(event);
4171
4191
  let elapsedSeconds = null;
4172
- if (shouldLookupElapsed) {
4173
- const sessionID = getSessionIDFromEvent(event);
4174
- if (sessionID) {
4192
+ let sessionTitle = preloadedSessionTitle ?? null;
4193
+ if (sessionID) {
4194
+ const minDuration = config.command?.minDuration;
4195
+ const shouldLookupElapsed = !!config.command?.enabled && typeof config.command?.path === "string" && config.command.path.length > 0 && typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0;
4196
+ if (shouldLookupElapsed) {
4175
4197
  elapsedSeconds = await getElapsedSinceLastPrompt(client, sessionID);
4176
4198
  }
4199
+ if (!sessionTitle && config.showSessionTitle) {
4200
+ const info = await getSessionInfo(client, sessionID);
4201
+ sessionTitle = info.title;
4202
+ }
4177
4203
  }
4178
- await handleEvent(config, eventType, projectName, elapsedSeconds);
4204
+ await handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle);
4179
4205
  }
4180
4206
  var NotifierPlugin = async ({ client, directory }) => {
4181
4207
  const config = loadConfig();
@@ -4191,18 +4217,24 @@ var NotifierPlugin = async ({ client, directory }) => {
4191
4217
  if (event.type === "session.idle") {
4192
4218
  const sessionID = getSessionIDFromEvent(event);
4193
4219
  if (sessionID) {
4194
- const isChild = await isChildSession(client, sessionID);
4195
- if (!isChild) {
4196
- await handleEventWithElapsedTime(client, config, "complete", projectName, event);
4220
+ const sessionInfo = await getSessionInfo(client, sessionID);
4221
+ if (!sessionInfo.isChild) {
4222
+ await handleEventWithElapsedTime(client, config, "complete", projectName, event, sessionInfo.title);
4197
4223
  } else {
4198
- await handleEventWithElapsedTime(client, config, "subagent_complete", projectName, event);
4224
+ await handleEventWithElapsedTime(client, config, "subagent_complete", projectName, event, sessionInfo.title);
4199
4225
  }
4200
4226
  } else {
4201
4227
  await handleEventWithElapsedTime(client, config, "complete", projectName, event);
4202
4228
  }
4203
4229
  }
4204
4230
  if (event.type === "session.error") {
4205
- await handleEventWithElapsedTime(client, config, "error", projectName, event);
4231
+ const sessionID = getSessionIDFromEvent(event);
4232
+ let sessionTitle = null;
4233
+ if (sessionID && config.showSessionTitle) {
4234
+ const info = await getSessionInfo(client, sessionID);
4235
+ sessionTitle = info.title;
4236
+ }
4237
+ await handleEventWithElapsedTime(client, config, "error", projectName, event, sessionTitle);
4206
4238
  }
4207
4239
  },
4208
4240
  "permission.ask": async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohak34/opencode-notifier",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
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",