@mohak34/opencode-notifier 0.1.32 → 0.1.34-beta

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 +19 -13
  2. package/dist/index.js +40 -27
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -55,6 +55,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
55
55
  "showSessionTitle": false,
56
56
  "showIcon": true,
57
57
  "suppressWhenFocused": true,
58
+ "enableOnDesktop": false,
58
59
  "notificationSystem": "osascript",
59
60
  "linux": {
60
61
  "grouping": false
@@ -66,12 +67,12 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
66
67
  "minDuration": 0
67
68
  },
68
69
  "events": {
69
- "permission": { "sound": true, "notification": true },
70
- "complete": { "sound": true, "notification": true },
71
- "subagent_complete": { "sound": false, "notification": false },
72
- "error": { "sound": true, "notification": true },
73
- "question": { "sound": true, "notification": true },
74
- "user_cancelled": { "sound": false, "notification": false }
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 }
75
76
  },
76
77
  "messages": {
77
78
  "permission": "Session needs permission: {sessionTitle}",
@@ -112,6 +113,8 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
112
113
  "showProjectName": true,
113
114
  "showSessionTitle": false,
114
115
  "showIcon": true,
116
+ "suppressWhenFocused": true,
117
+ "enableOnDesktop": false,
115
118
  "notificationSystem": "osascript"
116
119
  }
117
120
  ```
@@ -123,7 +126,8 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
123
126
  - `showSessionTitle` - Include the session title in notification messages via `{sessionTitle}` placeholder (default: true)
124
127
  - `showIcon` - Show OpenCode icon, Windows/Linux only (default: true)
125
128
  - `suppressWhenFocused` - Skip notifications and sounds when the terminal is the active window (default: true). See [Focus detection](#focus-detection) for platform details
126
- - `notificationSystem` - macOS only: `"osascript"`, `"node-notifier"`, or `"ghostty"` (default: "osascript"). Use `"ghostty"` if you're running Ghostty terminal for native OSC 777 notifications
129
+ - `enableOnDesktop` - Run the plugin on Desktop and Web clients (default: false). When false, the plugin only runs on CLI. Set to true if you want notifications/sounds/commands on Desktop/Web useful if you want custom commands (Telegram, webhooks) but don't care about built-in notifications
130
+ - `notificationSystem` - macOS only: `"osascript"`, `"node-notifier"`, or `"ghostty"` (default: "osascript"). Use `"ghostty"` if you're running Ghostty terminal for native OSC 9 notifications
127
131
  - `linux.grouping` - Linux only: replace notifications in-place instead of stacking (default: false). Requires `notify-send` 0.8+
128
132
 
129
133
  ### Events
@@ -133,18 +137,20 @@ Control each event separately:
133
137
  ```json
134
138
  {
135
139
  "events": {
136
- "permission": { "sound": true, "notification": true },
137
- "complete": { "sound": true, "notification": true },
138
- "subagent_complete": { "sound": false, "notification": false },
139
- "error": { "sound": true, "notification": true },
140
- "question": { "sound": true, "notification": true },
141
- "user_cancelled": { "sound": false, "notification": false }
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 }
142
146
  }
143
147
  }
144
148
  ```
145
149
 
146
150
  `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.
147
151
 
152
+ 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
+
148
154
  Or use true/false for both:
149
155
 
150
156
  ```json
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { basename } from "path";
3
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
3
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
4
4
 
5
5
  // src/config.ts
6
6
  import { readFileSync, existsSync } from "fs";
@@ -9,7 +9,8 @@ import { homedir } from "os";
9
9
  import { fileURLToPath } from "url";
10
10
  var DEFAULT_EVENT_CONFIG = {
11
11
  sound: true,
12
- notification: true
12
+ notification: true,
13
+ command: true
13
14
  };
14
15
  var DEFAULT_CONFIG = {
15
16
  sound: true,
@@ -19,6 +20,7 @@ var DEFAULT_CONFIG = {
19
20
  showSessionTitle: false,
20
21
  showIcon: true,
21
22
  suppressWhenFocused: true,
23
+ enableOnDesktop: false,
22
24
  notificationSystem: "osascript",
23
25
  linux: {
24
26
  grouping: false
@@ -31,11 +33,11 @@ var DEFAULT_CONFIG = {
31
33
  events: {
32
34
  permission: { ...DEFAULT_EVENT_CONFIG },
33
35
  complete: { ...DEFAULT_EVENT_CONFIG },
34
- subagent_complete: { sound: false, notification: false },
36
+ subagent_complete: { ...DEFAULT_EVENT_CONFIG, sound: false, notification: false },
35
37
  error: { ...DEFAULT_EVENT_CONFIG },
36
38
  question: { ...DEFAULT_EVENT_CONFIG },
37
39
  interrupted: { ...DEFAULT_EVENT_CONFIG },
38
- user_cancelled: { sound: false, notification: false }
40
+ user_cancelled: { ...DEFAULT_EVENT_CONFIG, sound: false, notification: false }
39
41
  },
40
42
  messages: {
41
43
  permission: "Session needs permission: {sessionTitle}",
@@ -82,12 +84,14 @@ function parseEventConfig(userEvent, defaultConfig) {
82
84
  if (typeof userEvent === "boolean") {
83
85
  return {
84
86
  sound: userEvent,
85
- notification: userEvent
87
+ notification: userEvent,
88
+ command: userEvent
86
89
  };
87
90
  }
88
91
  return {
89
92
  sound: userEvent.sound ?? defaultConfig.sound,
90
- notification: userEvent.notification ?? defaultConfig.notification
93
+ notification: userEvent.notification ?? defaultConfig.notification,
94
+ command: userEvent.command ?? defaultConfig.command
91
95
  };
92
96
  }
93
97
  function parseVolume(value, defaultVolume) {
@@ -114,7 +118,8 @@ function loadConfig() {
114
118
  const globalNotification = userConfig.notification ?? DEFAULT_CONFIG.notification;
115
119
  const defaultWithGlobal = {
116
120
  sound: globalSound,
117
- notification: globalNotification
121
+ notification: globalNotification,
122
+ command: true
118
123
  };
119
124
  const userCommand = userConfig.command ?? {};
120
125
  const commandArgs = Array.isArray(userCommand.args) ? userCommand.args.filter((arg) => typeof arg === "string") : undefined;
@@ -127,6 +132,7 @@ function loadConfig() {
127
132
  showSessionTitle: userConfig.showSessionTitle ?? DEFAULT_CONFIG.showSessionTitle,
128
133
  showIcon: userConfig.showIcon ?? DEFAULT_CONFIG.showIcon,
129
134
  suppressWhenFocused: userConfig.suppressWhenFocused ?? DEFAULT_CONFIG.suppressWhenFocused,
135
+ enableOnDesktop: typeof userConfig.enableOnDesktop === "boolean" ? userConfig.enableOnDesktop : DEFAULT_CONFIG.enableOnDesktop,
130
136
  notificationSystem: userConfig.notificationSystem === "node-notifier" ? "node-notifier" : userConfig.notificationSystem === "ghostty" ? "ghostty" : "osascript",
131
137
  linux: {
132
138
  grouping: typeof userConfig.linux?.grouping === "boolean" ? userConfig.linux.grouping : DEFAULT_CONFIG.linux.grouping
@@ -140,11 +146,11 @@ function loadConfig() {
140
146
  events: {
141
147
  permission: parseEventConfig(userConfig.events?.permission ?? userConfig.permission, defaultWithGlobal),
142
148
  complete: parseEventConfig(userConfig.events?.complete ?? userConfig.complete, defaultWithGlobal),
143
- subagent_complete: parseEventConfig(userConfig.events?.subagent_complete ?? userConfig.subagent_complete, { sound: false, notification: false }),
149
+ subagent_complete: parseEventConfig(userConfig.events?.subagent_complete ?? userConfig.subagent_complete, { sound: false, notification: false, command: true }),
144
150
  error: parseEventConfig(userConfig.events?.error ?? userConfig.error, defaultWithGlobal),
145
151
  question: parseEventConfig(userConfig.events?.question ?? userConfig.question, defaultWithGlobal),
146
152
  interrupted: parseEventConfig(userConfig.events?.interrupted ?? userConfig.interrupted, defaultWithGlobal),
147
- user_cancelled: parseEventConfig(userConfig.events?.user_cancelled ?? userConfig.user_cancelled, { sound: false, notification: false })
153
+ user_cancelled: parseEventConfig(userConfig.events?.user_cancelled ?? userConfig.user_cancelled, { sound: false, notification: false, command: true })
148
154
  },
149
155
  messages: {
150
156
  permission: userConfig.messages?.permission ?? DEFAULT_CONFIG.messages.permission,
@@ -184,6 +190,9 @@ function isEventSoundEnabled(config, event) {
184
190
  function isEventNotificationEnabled(config, event) {
185
191
  return config.events[event].notification;
186
192
  }
193
+ function isEventCommandEnabled(config, event) {
194
+ return config.events[event].command;
195
+ }
187
196
  function getMessage(config, event) {
188
197
  return config.messages[event];
189
198
  }
@@ -262,6 +271,7 @@ function detectNotifySendCapabilities() {
262
271
  function sendLinuxNotificationDirect(title, message, timeout, iconPath, grouping = true) {
263
272
  return new Promise((resolve) => {
264
273
  const args = [];
274
+ args.push("--app-name", "opencode");
265
275
  if (iconPath) {
266
276
  args.push("--icon", iconPath);
267
277
  }
@@ -336,7 +346,8 @@ async function sendNotification(title, message, timeout, iconPath, notificationS
336
346
  title,
337
347
  message,
338
348
  timeout,
339
- icon: iconPath
349
+ icon: iconPath,
350
+ "app-name": "opencode"
340
351
  };
341
352
  platformNotifier.notify(notificationOptions, () => {
342
353
  resolve();
@@ -420,7 +431,7 @@ async function playOnLinux(soundPath, volume) {
420
431
  const players = [
421
432
  { command: "paplay", args: [`--volume=${pulseVolume}`, soundPath] },
422
433
  { command: "aplay", args: [soundPath] },
423
- { command: "mpv", args: ["--no-video", "--no-terminal", `--volume=${percentVolume}`, soundPath] },
434
+ { command: "mpv", args: ["--no-video", "--no-terminal", "--script-opts=autoload-disabled=yes", `--volume=${percentVolume}`, soundPath] },
424
435
  { command: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", "-volume", `${percentVolume}`, soundPath] }
425
436
  ];
426
437
  for (const player of players) {
@@ -493,7 +504,7 @@ function runCommand2(config, event, message, sessionTitle, projectName, timestam
493
504
  }
494
505
 
495
506
  // src/focus.ts
496
- import { execSync } from "child_process";
507
+ import { execFileSync, execSync } from "child_process";
497
508
  function execWithTimeout(command, timeoutMs = 500) {
498
509
  try {
499
510
  return execSync(command, { timeout: timeoutMs, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
@@ -501,6 +512,13 @@ function execWithTimeout(command, timeoutMs = 500) {
501
512
  return null;
502
513
  }
503
514
  }
515
+ function execFileWithTimeout(command, args, timeoutMs = 500) {
516
+ try {
517
+ return execFileSync(command, args, { timeout: timeoutMs, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
518
+ } catch {
519
+ return null;
520
+ }
521
+ }
504
522
  function getHyprlandActiveWindowId() {
505
523
  const output = execWithTimeout("hyprctl activewindow -j");
506
524
  if (!output)
@@ -554,18 +572,11 @@ function getLinuxWaylandActiveWindowId() {
554
572
  return null;
555
573
  }
556
574
  function getWindowsActiveWindowId() {
557
- const script = `
558
- Add-Type @"
559
- using System;
560
- using System.Runtime.InteropServices;
561
- public class FocusHelper {
562
- [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
563
- }
564
- "@
565
- $hwnd = [FocusHelper]::GetForegroundWindow()
566
- Write-Output $hwnd
567
- `.trim().replace(/\n/g, "; ");
568
- return execWithTimeout(`powershell -NoProfile -Command "${script}"`, 1000);
575
+ const script = `$type=Add-Type -Name FocusHelper -Namespace OpenCodeNotifier -MemberDefinition '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();' -PassThru; $type::GetForegroundWindow()`;
576
+ let windowId = execFileWithTimeout("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], 1000);
577
+ if (!windowId)
578
+ windowId = execFileWithTimeout("pwsh", ["-NoProfile", "-NonInteractive", "-Command", script], 1000);
579
+ return windowId;
569
580
  }
570
581
  function getMacOSActiveWindowId() {
571
582
  return execWithTimeout(`osascript -e 'tell application "System Events" to return id of window 1 of (first application process whose frontmost is true)'`);
@@ -630,7 +641,7 @@ function loadTurnCount() {
630
641
  }
631
642
  function saveTurnCount(count) {
632
643
  try {
633
- writeFileSync2(getStatePath(), JSON.stringify({ turn: count }));
644
+ writeFileSync(getStatePath(), JSON.stringify({ turn: count }));
634
645
  } catch {}
635
646
  }
636
647
  function incrementTurnCount() {
@@ -697,7 +708,7 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds, sessi
697
708
  promises.push(playSound(eventType, customSoundPath, soundVolume));
698
709
  }
699
710
  const minDuration = config.command?.minDuration;
700
- const shouldSkipCommand = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
711
+ const shouldSkipCommand = !isEventCommandEnabled(config, eventType) || typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
701
712
  if (!shouldSkipCommand) {
702
713
  runCommand2(config, eventType, message, sessionTitle, projectName, timestamp, turn);
703
714
  }
@@ -838,7 +849,9 @@ async function handleEventWithElapsedTime(client, config, eventType, projectName
838
849
  var NotifierPlugin = async ({ client, directory }) => {
839
850
  const clientEnv = process.env.OPENCODE_CLIENT;
840
851
  if (clientEnv && clientEnv !== "cli") {
841
- return {};
852
+ const config = loadConfig();
853
+ if (!config.enableOnDesktop)
854
+ return {};
842
855
  }
843
856
  const getConfig = () => loadConfig();
844
857
  const projectName = directory ? basename(directory) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohak34/opencode-notifier",
3
- "version": "0.1.32",
3
+ "version": "0.1.34-beta",
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",