@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.
- package/README.md +16 -4
- package/dist/index.js +56 -25
- 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"
|
|
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}` -
|
|
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 `{
|
|
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 `{
|
|
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
|
-
|
|
4444
|
-
}
|
|
4445
|
-
if (platform3 === "linux") {
|
|
4459
|
+
windowFocused = isMacOSFocused(terminal);
|
|
4460
|
+
} else if (platform3 === "linux") {
|
|
4446
4461
|
if (process.env.WAYLAND_DISPLAY) {
|
|
4447
|
-
|
|
4448
|
-
}
|
|
4449
|
-
|
|
4450
|
-
|
|
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 (
|
|
4455
|
-
return
|
|
4456
|
-
|
|
4457
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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",
|