@mohak34/opencode-notifier 0.1.30-beta.2 → 0.1.30-beta.4

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 +16 -4
  2. package/dist/index.js +56 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -123,7 +123,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
123
123
  - `showSessionTitle` - Include the session title in notification messages via `{sessionTitle}` placeholder (default: true)
124
124
  - `showIcon` - Show OpenCode icon, Windows/Linux only (default: true)
125
125
  - `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"` or `"node-notifier"` (default: "osascript")
126
+ - `notificationSystem` - macOS only: `"osascript"`, `"node-notifier"`, or `"ghostty"` (default: "osascript"). Use `"ghostty"` if you're running Ghostty terminal for native OSC 777 notifications
127
127
  - `linux.grouping` - Linux only: replace notifications in-place instead of stacking (default: false). Requires `notify-send` 0.8+
128
128
 
129
129
  ### Events
@@ -177,7 +177,7 @@ Messages support placeholder tokens that get replaced with actual values:
177
177
  - `{sessionTitle}` - The title/summary of the current session (e.g. "Fix login bug")
178
178
  - `{projectName}` - The project folder name
179
179
  - `{timestamp}` - Current time in `HH:MM:SS` format (e.g. "14:30:05")
180
- - `{turn}` - Notification counter for the session, increments with each notification (e.g. 1, 2, 3)
180
+ - `{turn}` - Global notification counter that persists across restarts (e.g. 1, 2, 3). Stored in `~/.config/opencode/opencode-notifier-state.json`
181
181
 
182
182
  When `showSessionTitle` is `false`, `{sessionTitle}` is replaced with an empty string. Any trailing separators (`: `, ` - `, ` | `) are automatically cleaned up when a placeholder resolves to empty.
183
183
 
@@ -230,7 +230,7 @@ Set per-event volume from `0` to `1`:
230
230
 
231
231
  ### Custom commands
232
232
 
233
- Run your own script when something happens. Use `{event}`, `{message}`, and `{sessionTitle}` as placeholders:
233
+ Run your own script when something happens. Use `{event}`, `{message}`, `{sessionTitle}`, `{projectName}`, `{timestamp}`, and `{turn}` as placeholders:
234
234
 
235
235
  ```json
236
236
  {
@@ -245,7 +245,7 @@ Run your own script when something happens. Use `{event}`, `{message}`, and `{se
245
245
 
246
246
  - `enabled` - Turn command on/off
247
247
  - `path` - Path to your script/executable
248
- - `args` - Arguments to pass, can use `{event}`, `{message}`, and `{sessionTitle}` tokens
248
+ - `args` - Arguments to pass, can use `{event}`, `{message}`, `{sessionTitle}`, `{projectName}`, `{timestamp}`, and `{turn}` tokens
249
249
  - `minDuration` - Skip if response was quick, avoids spam (seconds)
250
250
 
251
251
  #### Example: Log events to a file
@@ -283,6 +283,18 @@ Run your own script when something happens. Use `{event}`, `{message}`, and `{se
283
283
 
284
284
  **NOTE:** If you go with node-notifier and start missing notifications, just switch back or remove the option from the config. Users have reported issues with using node-notifier for receiving only sounds and no notification popups.
285
285
 
286
+ ## Ghostty notifications
287
+
288
+ If you're using [Ghostty](https://ghostty.org/) terminal, you can use its native notification system via OSC 777 escape sequences:
289
+
290
+ ```json
291
+ {
292
+ "notificationSystem": "ghostty"
293
+ }
294
+ ```
295
+
296
+ This sends notifications directly through the terminal instead of using system notification tools. Works on any platform where Ghostty is running.
297
+
286
298
  ## Focus detection
287
299
 
288
300
  When `suppressWhenFocused` is `true` (the default), notifications and sounds are skipped if the terminal running OpenCode is the active/focused window. The idea is simple: if you're already looking at it, you don't need an alert.
package/dist/index.js CHANGED
@@ -3728,6 +3728,7 @@ var require_node_notifier = __commonJS((exports, module) => {
3728
3728
 
3729
3729
  // src/index.ts
3730
3730
  import { basename } from "path";
3731
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3731
3732
 
3732
3733
  // src/config.ts
3733
3734
  import { readFileSync, existsSync } from "fs";
@@ -3798,6 +3799,10 @@ function getConfigPath() {
3798
3799
  }
3799
3800
  return join(homedir(), ".config", "opencode", "opencode-notifier.json");
3800
3801
  }
3802
+ function getStatePath() {
3803
+ const configPath = getConfigPath();
3804
+ return join(dirname(configPath), "opencode-notifier-state.json");
3805
+ }
3801
3806
  function parseEventConfig(userEvent, defaultConfig) {
3802
3807
  if (userEvent === undefined) {
3803
3808
  return defaultConfig;
@@ -4314,6 +4319,16 @@ function isPidAncestorOfProcess(targetPid, startPid) {
4314
4319
  function isFocusedWindowOurs(windowPid) {
4315
4320
  return isPidAncestorOfProcess(windowPid, getProcessTreeRoot());
4316
4321
  }
4322
+ var tmuxPane = process.env.TMUX_PANE ?? null;
4323
+ function isTmuxPaneActive() {
4324
+ if (!tmuxPane)
4325
+ return true;
4326
+ const result = execWithTimeout(`tmux display-message -t ${tmuxPane} -p '#{window_active} #{pane_active}'`);
4327
+ if (!result)
4328
+ return false;
4329
+ const [windowActive, paneActive] = result.split(" ");
4330
+ return windowActive === "1" && paneActive === "1";
4331
+ }
4317
4332
  function isLinuxX11Focused() {
4318
4333
  const pidStr = execWithTimeout("xdotool getactivewindow getwindowpid");
4319
4334
  if (!pidStr)
@@ -4436,25 +4451,30 @@ Write-Output $pid
4436
4451
  function isTerminalFocused() {
4437
4452
  try {
4438
4453
  const platform3 = process.platform;
4454
+ let windowFocused = false;
4439
4455
  if (platform3 === "darwin") {
4440
4456
  const terminal = detectTerminal();
4441
4457
  if (!terminal || terminal.macProcessNames.length === 0)
4442
4458
  return false;
4443
- return isMacOSFocused(terminal);
4444
- }
4445
- if (platform3 === "linux") {
4459
+ windowFocused = isMacOSFocused(terminal);
4460
+ } else if (platform3 === "linux") {
4446
4461
  if (process.env.WAYLAND_DISPLAY) {
4447
- return isLinuxWaylandFocused();
4448
- }
4449
- if (process.env.DISPLAY) {
4450
- return isLinuxX11Focused();
4462
+ windowFocused = isLinuxWaylandFocused();
4463
+ } else if (process.env.DISPLAY) {
4464
+ windowFocused = isLinuxX11Focused();
4465
+ } else {
4466
+ return false;
4451
4467
  }
4468
+ } else if (platform3 === "win32") {
4469
+ windowFocused = isWindowsFocused();
4470
+ } else {
4452
4471
  return false;
4453
4472
  }
4454
- if (platform3 === "win32") {
4455
- return isWindowsFocused();
4456
- }
4457
- return false;
4473
+ if (!windowFocused)
4474
+ return false;
4475
+ if (process.env.TMUX)
4476
+ return isTmuxPaneActive();
4477
+ return true;
4458
4478
  } catch {
4459
4479
  return false;
4460
4480
  }
@@ -4466,7 +4486,30 @@ var pendingIdleTimers = new Map;
4466
4486
  var sessionIdleSequence = new Map;
4467
4487
  var sessionErrorSuppressionAt = new Map;
4468
4488
  var sessionLastBusyAt = new Map;
4469
- var sessionTurnCount = new Map;
4489
+ var globalTurnCount = null;
4490
+ function loadTurnCount() {
4491
+ try {
4492
+ const content = readFileSync3(getStatePath(), "utf-8");
4493
+ const state = JSON.parse(content);
4494
+ if (typeof state.turn === "number" && Number.isFinite(state.turn) && state.turn >= 0) {
4495
+ return state.turn;
4496
+ }
4497
+ } catch {}
4498
+ return 0;
4499
+ }
4500
+ function saveTurnCount(count) {
4501
+ try {
4502
+ writeFileSync2(getStatePath(), JSON.stringify({ turn: count }));
4503
+ } catch {}
4504
+ }
4505
+ function incrementTurnCount() {
4506
+ if (globalTurnCount === null) {
4507
+ globalTurnCount = loadTurnCount();
4508
+ }
4509
+ globalTurnCount++;
4510
+ saveTurnCount(globalTurnCount);
4511
+ return globalTurnCount;
4512
+ }
4470
4513
  setInterval(() => {
4471
4514
  const cutoff = Date.now() - 5 * 60 * 1000;
4472
4515
  for (const [sessionID] of sessionIdleSequence) {
@@ -4484,11 +4527,6 @@ setInterval(() => {
4484
4527
  sessionLastBusyAt.delete(sessionID);
4485
4528
  }
4486
4529
  }
4487
- for (const [sessionID] of sessionTurnCount) {
4488
- if (!pendingIdleTimers.has(sessionID) && !sessionLastBusyAt.has(sessionID)) {
4489
- sessionTurnCount.delete(sessionID);
4490
- }
4491
- }
4492
4530
  }, 5 * 60 * 1000);
4493
4531
  function getNotificationTitle(config, projectName) {
4494
4532
  if (config.showProjectName && projectName) {
@@ -4503,20 +4541,13 @@ function formatTimestamp() {
4503
4541
  const s = String(now.getSeconds()).padStart(2, "0");
4504
4542
  return `${h}:${m}:${s}`;
4505
4543
  }
4506
- function incrementSessionTurn(sessionID) {
4507
- if (!sessionID)
4508
- return 1;
4509
- const next = (sessionTurnCount.get(sessionID) ?? 0) + 1;
4510
- sessionTurnCount.set(sessionID, next);
4511
- return next;
4512
- }
4513
4544
  async function handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle, sessionID) {
4514
4545
  if (config.suppressWhenFocused && isTerminalFocused()) {
4515
4546
  return;
4516
4547
  }
4517
4548
  const promises = [];
4518
4549
  const timestamp = formatTimestamp();
4519
- const turn = incrementSessionTurn(sessionID ?? null);
4550
+ const turn = incrementTurnCount();
4520
4551
  const rawMessage = getMessage(config, eventType);
4521
4552
  const message = interpolateMessage(rawMessage, {
4522
4553
  sessionTitle: config.showSessionTitle ? sessionTitle : null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohak34/opencode-notifier",
3
- "version": "0.1.30-beta.2",
3
+ "version": "0.1.30-beta.4",
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",