@mohak34/opencode-notifier 0.1.14 → 0.1.16

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 +11 -1
  2. package/dist/index.js +332 -2
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -92,6 +92,7 @@ To customize the plugin, create `~/.config/opencode/opencode-notifier.json`:
92
92
  "notification": true,
93
93
  "timeout": 5,
94
94
  "showProjectName": true,
95
+ "suppressWhenFocused": false,
95
96
  "command": {
96
97
  "enabled": false,
97
98
  "path": "/path/to/command",
@@ -108,7 +109,7 @@ To customize the plugin, create `~/.config/opencode/opencode-notifier.json`:
108
109
  "messages": {
109
110
  "permission": "Session needs permission",
110
111
  "complete": "Session has finished",
111
- "subagent_complete": "Subagent has finished",
112
+ "subagent_complete": "Subagent task completed",
112
113
  "error": "Session encountered an error",
113
114
  "question": "Session has a question"
114
115
  },
@@ -130,6 +131,7 @@ To customize the plugin, create `~/.config/opencode/opencode-notifier.json`:
130
131
  | `notification` | boolean | `true` | Global toggle for all notifications |
131
132
  | `timeout` | number | `5` | Notification duration in seconds (Linux only) |
132
133
  | `showProjectName` | boolean | `true` | Show project folder name in notification title |
134
+ | `suppressWhenFocused` | boolean | `false` | Suppress notifications when terminal is focused |
133
135
  | `command` | object | — | Command execution settings (enabled/path/args/minDuration) |
134
136
 
135
137
  ### Events
@@ -164,6 +166,14 @@ Or use a boolean to toggle both:
164
166
 
165
167
  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
168
 
169
+ ### Suppress when focused
170
+
171
+ When `suppressWhenFocused` is enabled, notifications are skipped if your terminal is the frontmost window.
172
+
173
+ - macOS: works out of the box
174
+ - Windows: uses PowerShell to detect the foreground app
175
+ - Linux: requires one of `xdotool`, `wmctrl`, or `xprop` (install via your package manager). If none are installed, suppression is skipped.
176
+
167
177
  ### Messages
168
178
 
169
179
  Customize notification text:
package/dist/index.js CHANGED
@@ -18,6 +18,231 @@ var __toESM = (mod, isNodeMode, target) => {
18
18
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
+ // node_modules/detect-terminal/dist/index.js
22
+ var require_dist = __commonJS((exports, module) => {
23
+ var __create2 = Object.create;
24
+ var __defProp2 = Object.defineProperty;
25
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
26
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
27
+ var __getProtoOf2 = Object.getPrototypeOf;
28
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
29
+ var __name = (target, value) => __defProp2(target, "name", { value, configurable: true });
30
+ var __export = (target, all) => {
31
+ for (var name in all)
32
+ __defProp2(target, name, { get: all[name], enumerable: true });
33
+ };
34
+ var __copyProps = (to, from, except, desc) => {
35
+ if (from && typeof from === "object" || typeof from === "function") {
36
+ for (let key of __getOwnPropNames2(from))
37
+ if (!__hasOwnProp2.call(to, key) && key !== except)
38
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
39
+ }
40
+ return to;
41
+ };
42
+ var __toESM2 = (mod, isNodeMode, target) => (target = mod != null ? __create2(__getProtoOf2(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp2(target, "default", { value: mod, enumerable: true }) : target, mod));
43
+ var __toCommonJS = (mod) => __copyProps(__defProp2({}, "__esModule", { value: true }), mod);
44
+ var detect_terminal_exports = {};
45
+ __export(detect_terminal_exports, {
46
+ default: () => detect_terminal_default,
47
+ detectFromEnv: () => detectFromEnv,
48
+ detectFromProcessTitle: () => detectFromProcessTitle,
49
+ detectFromShell: () => detectFromShell,
50
+ detectTerminal: () => detectTerminal
51
+ });
52
+ module.exports = __toCommonJS(detect_terminal_exports);
53
+ var import_node_child_process = __require("child_process");
54
+ var import_node_path = __toESM2(__require("path"));
55
+ var detectFromEnv = /* @__PURE__ */ __name(() => {
56
+ const platform = process.platform;
57
+ if (platform === "android" && process.env.TERMUX_VERSION) {
58
+ return "termux";
59
+ }
60
+ let termProgram = process.env.TERM_PROGRAM?.trim()?.toLowerCase();
61
+ if (platform === "darwin" && termProgram) {
62
+ termProgram = import_node_path.default.parse(termProgram).name;
63
+ }
64
+ if (termProgram) {
65
+ switch (termProgram) {
66
+ case "apple_terminal":
67
+ return "terminal";
68
+ case "eterm":
69
+ return "eterm";
70
+ case "gnome-terminal":
71
+ return "gnome_terminal";
72
+ case "gnome-terminal-server":
73
+ return "gnome_terminal";
74
+ case "hyper":
75
+ return "hyper";
76
+ case "iterm.app":
77
+ return "iterm";
78
+ case "iterm":
79
+ return "iterm";
80
+ case "iterm2":
81
+ return "iterm";
82
+ case "kitty":
83
+ return "kitty";
84
+ case "konsole":
85
+ return "konsole";
86
+ case "mate-terminal":
87
+ return "mate_terminal";
88
+ case "powershell":
89
+ return "powershell";
90
+ case "putty":
91
+ return "putty";
92
+ case "qterminal":
93
+ return "qterminal";
94
+ case "rxvt":
95
+ return "rxvt";
96
+ case "terminal.app":
97
+ return "terminal_app";
98
+ case "terminal":
99
+ return "terminal";
100
+ case "terminator":
101
+ return "terminator";
102
+ case "termux":
103
+ return "termux";
104
+ case "vscode":
105
+ return "vscode";
106
+ case "warp":
107
+ return "warp";
108
+ case "wezterm":
109
+ return "wezterm";
110
+ case "xfce4-terminal":
111
+ return "xfce4_terminal";
112
+ case "alacritty":
113
+ return "alacritty";
114
+ default:
115
+ break;
116
+ }
117
+ return termProgram.replace(/[^a-z0-9]+/g, "_");
118
+ }
119
+ if (typeof process.env.VSCODE_PID !== "undefined" || typeof process.env.TERM_PROGRAM_VERSION !== "undefined" && /vscode/i.test(process.env.TERM_PROGRAM_VERSION)) {
120
+ return "vscode";
121
+ }
122
+ const term = process.env.TERM?.trim()?.toLowerCase();
123
+ if (term && term !== "unknown") {
124
+ if (term === "xterm" || term === "xterm-256color") {
125
+ if (process.env.VTE_VERSION) {
126
+ const vteVersion = parseInt(process.env.VTE_VERSION, 10);
127
+ if (vteVersion >= 3803) {
128
+ return "gnome_terminal";
129
+ }
130
+ }
131
+ for (const [key] of Object.entries(process.env)) {
132
+ if (/konsole/i.test(key)) {
133
+ return "konsole";
134
+ }
135
+ }
136
+ if (platform === "darwin") {
137
+ return "terminal";
138
+ }
139
+ return "xterm";
140
+ }
141
+ if (/screen/.test(term))
142
+ return "screen";
143
+ if (/tmux/.test(term))
144
+ return "tmux";
145
+ if (/rxvt/.test(term))
146
+ return "rxvt";
147
+ if (/vt100/.test(term))
148
+ return "vt100";
149
+ if (/linux/.test(term))
150
+ return "linux_console";
151
+ if (/alacritty/.test(term))
152
+ return "alacritty";
153
+ if (/dopamine/.test(term))
154
+ return "dopamine";
155
+ if (/kitty/.test(term))
156
+ return "kitty";
157
+ if (/ghostty/.test(term))
158
+ return "ghostty";
159
+ return term.replace(/[^a-z0-9]+/g, "_");
160
+ }
161
+ const colorTerm = process.env.COLORTERM?.trim()?.toLowerCase();
162
+ if (colorTerm === "truecolor" || colorTerm === "24bit") {
163
+ return "truecolor_terminal";
164
+ }
165
+ if (colorTerm) {
166
+ return colorTerm.replace(/[^a-z0-9]+/g, "_");
167
+ }
168
+ return null;
169
+ }, "detectFromEnv");
170
+ var detectFromShell = /* @__PURE__ */ __name(() => {
171
+ try {
172
+ const isWindows = process.platform === "win32";
173
+ if (isWindows) {
174
+ if (process.env.WT_SESSION)
175
+ return "windows_terminal";
176
+ const shell = process.env.COMSPEC?.toLowerCase() || "";
177
+ if (/powershell/i.test(shell))
178
+ return "powershell";
179
+ if (/pwsh/i.test(shell))
180
+ return "powershell";
181
+ if (/cmd\.exe/i.test(shell))
182
+ return "cmd";
183
+ if (/wt\.exe/i.test(shell))
184
+ return "windows_terminal";
185
+ if (/conhost\.exe/i.test(shell))
186
+ return "conhost";
187
+ return "windows_cmd";
188
+ }
189
+ const terminal = (0, import_node_child_process.execSync)("echo $TERM", {
190
+ encoding: "utf8",
191
+ timeout: 1000,
192
+ stdio: ["ignore", "pipe", "ignore"]
193
+ }).trim().toLowerCase();
194
+ return terminal ? terminal.replace(/[^a-z0-9]+/g, "_") : null;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }, "detectFromShell");
199
+ var detectFromProcessTitle = /* @__PURE__ */ __name(() => {
200
+ const processTitle = process.title?.toLowerCase() ?? "";
201
+ if (/^alacritty/.test(processTitle))
202
+ return "alacritty";
203
+ if (/^kitty/.test(processTitle))
204
+ return "kitty";
205
+ if (/^wezterm/.test(processTitle))
206
+ return "wezterm";
207
+ if (/^hyper/.test(processTitle))
208
+ return "hyper";
209
+ if (/bash/.test(processTitle))
210
+ return "bash";
211
+ if (/zsh/.test(processTitle))
212
+ return "zsh";
213
+ if (/ksh/.test(processTitle))
214
+ return "ksh";
215
+ if (/fish/.test(processTitle))
216
+ return "fish";
217
+ if (/csh/.test(processTitle))
218
+ return "csh";
219
+ if (/tcsh/.test(processTitle))
220
+ return "tcsh";
221
+ if (/pwsh/.test(processTitle))
222
+ return "powershell";
223
+ if (/powershell/.test(processTitle))
224
+ return "powershell";
225
+ if (/cmd/.test(processTitle))
226
+ return "cmd";
227
+ if (/sh$/.test(processTitle))
228
+ return "sh";
229
+ if (/^node/.test(processTitle))
230
+ return "node";
231
+ return null;
232
+ }, "detectFromProcessTitle");
233
+ var detectTerminal = /* @__PURE__ */ __name(() => {
234
+ let terminal = detectFromEnv();
235
+ if (!terminal) {
236
+ terminal = detectFromShell();
237
+ }
238
+ if (!terminal) {
239
+ terminal = detectFromProcessTitle();
240
+ }
241
+ return terminal || "unknown";
242
+ }, "detectTerminal");
243
+ var detect_terminal_default = detectTerminal;
244
+ });
245
+
21
246
  // node_modules/shellwords/lib/shellwords.js
22
247
  var require_shellwords = __commonJS((exports) => {
23
248
  (function() {
@@ -3502,7 +3727,7 @@ var require_version = __commonJS((exports) => {
3502
3727
  });
3503
3728
 
3504
3729
  // node_modules/uuid/dist/index.js
3505
- var require_dist = __commonJS((exports) => {
3730
+ var require_dist2 = __commonJS((exports) => {
3506
3731
  Object.defineProperty(exports, "__esModule", {
3507
3732
  value: true
3508
3733
  });
@@ -3582,7 +3807,7 @@ var require_toaster = __commonJS((exports, module) => {
3582
3807
  var utils = require_utils();
3583
3808
  var Balloon = require_balloon();
3584
3809
  var os = __require("os");
3585
- var { v4: uuid } = require_dist();
3810
+ var { v4: uuid } = require_dist2();
3586
3811
  var EventEmitter = __require("events").EventEmitter;
3587
3812
  var util = __require("util");
3588
3813
  var fallback;
@@ -3742,6 +3967,7 @@ var DEFAULT_CONFIG = {
3742
3967
  notification: true,
3743
3968
  timeout: 5,
3744
3969
  showProjectName: true,
3970
+ suppressWhenFocused: false,
3745
3971
  command: {
3746
3972
  enabled: false,
3747
3973
  path: "",
@@ -3809,6 +4035,7 @@ function loadConfig() {
3809
4035
  notification: globalNotification,
3810
4036
  timeout: typeof userConfig.timeout === "number" && userConfig.timeout > 0 ? userConfig.timeout : DEFAULT_CONFIG.timeout,
3811
4037
  showProjectName: userConfig.showProjectName ?? DEFAULT_CONFIG.showProjectName,
4038
+ suppressWhenFocused: typeof userConfig.suppressWhenFocused === "boolean" ? userConfig.suppressWhenFocused : DEFAULT_CONFIG.suppressWhenFocused,
3812
4039
  command: {
3813
4040
  enabled: typeof userCommand.enabled === "boolean" ? userCommand.enabled : DEFAULT_CONFIG.command.enabled,
3814
4041
  path: typeof userCommand.path === "string" ? userCommand.path : DEFAULT_CONFIG.command.path,
@@ -3854,6 +4081,10 @@ function getSoundPath(config, event) {
3854
4081
  return config.sounds[event];
3855
4082
  }
3856
4083
 
4084
+ // src/index.ts
4085
+ var import_detect_terminal = __toESM(require_dist(), 1);
4086
+ import { execFile } from "child_process";
4087
+
3857
4088
  // src/notify.ts
3858
4089
  var import_node_notifier = __toESM(require_node_notifier(), 1);
3859
4090
  import os from "os";
@@ -4030,6 +4261,12 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds) {
4030
4261
  const promises = [];
4031
4262
  const message = getMessage(config, eventType);
4032
4263
  if (isEventNotificationEnabled(config, eventType)) {
4264
+ if (config.suppressWhenFocused) {
4265
+ const shouldSuppress = await isTerminalFocused();
4266
+ if (shouldSuppress) {
4267
+ return;
4268
+ }
4269
+ }
4033
4270
  const title = getNotificationTitle(config, projectName);
4034
4271
  promises.push(sendNotification(title, message, config.timeout));
4035
4272
  }
@@ -4044,6 +4281,99 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds) {
4044
4281
  }
4045
4282
  await Promise.allSettled(promises);
4046
4283
  }
4284
+ async function runExecCommand(command, args) {
4285
+ return new Promise((resolve) => {
4286
+ execFile(command, args, (error, stdout) => {
4287
+ if (error) {
4288
+ resolve(null);
4289
+ return;
4290
+ }
4291
+ resolve(stdout.trim());
4292
+ });
4293
+ });
4294
+ }
4295
+ async function runOsascript(script) {
4296
+ if (process.platform !== "darwin")
4297
+ return null;
4298
+ return runExecCommand("osascript", ["-e", script]);
4299
+ }
4300
+ async function getFrontmostAppMac() {
4301
+ return runOsascript('tell application "System Events" to get name of first application process whose frontmost is true');
4302
+ }
4303
+ async function getFrontmostProcessWindows() {
4304
+ if (process.platform !== "win32")
4305
+ return null;
4306
+ const script = [
4307
+ 'Add-Type @"',
4308
+ "using System;",
4309
+ "using System.Runtime.InteropServices;",
4310
+ "public class Win32 {",
4311
+ ' [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',
4312
+ ' [DllImport("user32.dll")] public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int processId);',
4313
+ "}",
4314
+ '"@',
4315
+ "$hwnd = [Win32]::GetForegroundWindow()",
4316
+ "$pid = 0",
4317
+ "[Win32]::GetWindowThreadProcessId($hwnd, [ref]$pid) | Out-Null",
4318
+ "(Get-Process -Id $pid).ProcessName"
4319
+ ].join(`
4320
+ `);
4321
+ return runExecCommand("powershell", ["-NoProfile", "-Command", script]);
4322
+ }
4323
+ function extractQuotedStrings(input) {
4324
+ const matches = input.match(/"([^"]+)"/g);
4325
+ if (!matches)
4326
+ return [];
4327
+ return matches.map((value) => value.replace(/"/g, ""));
4328
+ }
4329
+ async function getActiveWindowClassLinux() {
4330
+ if (process.platform !== "linux")
4331
+ return null;
4332
+ const xdotoolClass = await runExecCommand("xdotool", ["getactivewindow", "getwindowclassname"]);
4333
+ if (xdotoolClass)
4334
+ return xdotoolClass;
4335
+ const activeWindow = await runExecCommand("xprop", ["-root", "_NET_ACTIVE_WINDOW"]);
4336
+ if (!activeWindow)
4337
+ return null;
4338
+ const idMatch = activeWindow.match(/0x[0-9a-fA-F]+/);
4339
+ if (!idMatch)
4340
+ return null;
4341
+ const wmClass = await runExecCommand("xprop", ["-id", idMatch[0], "WM_CLASS"]);
4342
+ if (!wmClass)
4343
+ return null;
4344
+ const names = extractQuotedStrings(wmClass);
4345
+ if (names.length === 0)
4346
+ return null;
4347
+ return names[names.length - 1];
4348
+ }
4349
+ function normalizeAppName(value) {
4350
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
4351
+ }
4352
+ function matchesTerminal(frontmost, terminal) {
4353
+ const frontNormalized = normalizeAppName(frontmost);
4354
+ const terminalNormalized = normalizeAppName(terminal);
4355
+ if (!frontNormalized || !terminalNormalized)
4356
+ return false;
4357
+ return frontNormalized.includes(terminalNormalized) || terminalNormalized.includes(frontNormalized);
4358
+ }
4359
+ async function isTerminalFocused() {
4360
+ const terminalName = import_detect_terminal.default({ preferOuter: true });
4361
+ if (!terminalName)
4362
+ return false;
4363
+ if (process.platform === "darwin") {
4364
+ const frontmost = await getFrontmostAppMac();
4365
+ return frontmost ? matchesTerminal(frontmost, terminalName) : false;
4366
+ }
4367
+ if (process.platform === "win32") {
4368
+ const frontmostProcess = await getFrontmostProcessWindows();
4369
+ return frontmostProcess ? matchesTerminal(frontmostProcess, terminalName) : false;
4370
+ }
4371
+ if (process.platform === "linux") {
4372
+ const frontmostClass = await getActiveWindowClassLinux();
4373
+ return frontmostClass ? matchesTerminal(frontmostClass, terminalName) : false;
4374
+ }
4375
+ return false;
4376
+ }
4047
4377
  function getSessionIDFromEvent(event) {
4048
4378
  const sessionID = event?.properties?.sessionID;
4049
4379
  if (typeof sessionID === "string" && sessionID.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohak34/opencode-notifier",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
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",
@@ -32,6 +32,7 @@
32
32
  "alerts"
33
33
  ],
34
34
  "dependencies": {
35
+ "detect-terminal": "^2.0.0",
35
36
  "node-notifier": "^10.0.1"
36
37
  },
37
38
  "devDependencies": {