@mohak34/opencode-notifier 0.1.30-beta.1 → 0.1.30-beta.3
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 +22 -11
- package/dist/index.js +64 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -176,11 +176,15 @@ Messages support placeholder tokens that get replaced with actual values:
|
|
|
176
176
|
|
|
177
177
|
- `{sessionTitle}` - The title/summary of the current session (e.g. "Fix login bug")
|
|
178
178
|
- `{projectName}` - The project folder name
|
|
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)
|
|
179
181
|
|
|
180
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.
|
|
181
183
|
|
|
182
184
|
To disable session titles in messages without changing `showSessionTitle`, just remove the `{sessionTitle}` placeholder from your custom messages.
|
|
183
185
|
|
|
186
|
+
The `{timestamp}` and `{turn}` placeholders also work in custom command args.
|
|
187
|
+
|
|
184
188
|
### Sounds
|
|
185
189
|
|
|
186
190
|
Use your own sound files:
|
|
@@ -293,17 +297,24 @@ To disable this and always get notified:
|
|
|
293
297
|
|
|
294
298
|
### Platform support
|
|
295
299
|
|
|
296
|
-
| Platform | Method | Requirements |
|
|
297
|
-
|
|
298
|
-
| macOS | AppleScript (`System Events`) | None |
|
|
299
|
-
| Linux X11 | `xdotool` | `xdotool` installed |
|
|
300
|
-
| Linux Wayland (Hyprland) | `hyprctl activewindow` | None |
|
|
301
|
-
| Linux Wayland (Sway) | `swaymsg -t get_tree` | None |
|
|
302
|
-
| Linux Wayland (KDE) | `kdotool` | `kdotool` installed |
|
|
303
|
-
| Linux Wayland (GNOME) | Not supported | Falls back to always notifying |
|
|
304
|
-
|
|
|
305
|
-
|
|
306
|
-
|
|
300
|
+
| Platform | Method | Requirements | Status |
|
|
301
|
+
|----------|--------|--------------|--------|
|
|
302
|
+
| macOS | AppleScript (`System Events`) | None | Untested |
|
|
303
|
+
| Linux X11 | `xdotool` | `xdotool` installed | Untested |
|
|
304
|
+
| Linux Wayland (Hyprland) | `hyprctl activewindow` | None | Tested |
|
|
305
|
+
| Linux Wayland (Sway) | `swaymsg -t get_tree` | None | Untested |
|
|
306
|
+
| Linux Wayland (KDE) | `kdotool` | `kdotool` installed | Untested |
|
|
307
|
+
| Linux Wayland (GNOME) | Not supported | - | Falls back to always notifying |
|
|
308
|
+
| Linux Wayland (Niri, river, dwl, Cosmic, etc.) | Not supported | - | Falls back to always notifying |
|
|
309
|
+
| Windows | `GetForegroundWindow()` via PowerShell | None | Untested |
|
|
310
|
+
|
|
311
|
+
**Unsupported compositors**: Wayland has no standard protocol for querying the focused window. Each compositor has its own IPC, and GNOME intentionally doesn't expose focus information. Unsupported compositors fall back to always notifying.
|
|
312
|
+
|
|
313
|
+
**tmux/screen**: When running inside tmux, the tmux server daemonizes and detaches from the terminal's process tree. The plugin handles this by querying the tmux client PID and walking from there. GNU Screen has the same issue but is not currently handled (falls back to always notifying).
|
|
314
|
+
|
|
315
|
+
**Fail-open design**: If detection fails for any reason (missing tools, unknown compositor, permissions), it falls back to always notifying. It never silently eats your notifications.
|
|
316
|
+
|
|
317
|
+
If you test on a platform marked "Untested" and it works (or doesn't), please open an issue and let us know.
|
|
307
318
|
|
|
308
319
|
## Linux: Notification Grouping
|
|
309
320
|
|
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;
|
|
@@ -3850,7 +3855,7 @@ function loadConfig() {
|
|
|
3850
3855
|
showSessionTitle: userConfig.showSessionTitle ?? DEFAULT_CONFIG.showSessionTitle,
|
|
3851
3856
|
showIcon: userConfig.showIcon ?? DEFAULT_CONFIG.showIcon,
|
|
3852
3857
|
suppressWhenFocused: userConfig.suppressWhenFocused ?? DEFAULT_CONFIG.suppressWhenFocused,
|
|
3853
|
-
notificationSystem: userConfig.notificationSystem === "node-notifier" ? "node-notifier" : "osascript",
|
|
3858
|
+
notificationSystem: userConfig.notificationSystem === "node-notifier" ? "node-notifier" : userConfig.notificationSystem === "ghostty" ? "ghostty" : "osascript",
|
|
3854
3859
|
linux: {
|
|
3855
3860
|
grouping: typeof userConfig.linux?.grouping === "boolean" ? userConfig.linux.grouping : DEFAULT_CONFIG.linux.grouping
|
|
3856
3861
|
},
|
|
@@ -3936,6 +3941,10 @@ function interpolateMessage(message, context) {
|
|
|
3936
3941
|
result = result.replaceAll("{sessionTitle}", sessionTitle);
|
|
3937
3942
|
const projectName = context.projectName || "";
|
|
3938
3943
|
result = result.replaceAll("{projectName}", projectName);
|
|
3944
|
+
const timestamp = context.timestamp || "";
|
|
3945
|
+
result = result.replaceAll("{timestamp}", timestamp);
|
|
3946
|
+
const turn = context.turn != null ? String(context.turn) : "";
|
|
3947
|
+
result = result.replaceAll("{turn}", turn);
|
|
3939
3948
|
result = result.replace(/\s*[:\-|]\s*$/, "").trim();
|
|
3940
3949
|
result = result.replace(/\s{2,}/g, " ");
|
|
3941
3950
|
return result;
|
|
@@ -4009,6 +4018,15 @@ async function sendNotification(title, message, timeout, iconPath, notificationS
|
|
|
4009
4018
|
return;
|
|
4010
4019
|
}
|
|
4011
4020
|
lastNotificationTime[message] = now;
|
|
4021
|
+
if (notificationSystem === "ghostty") {
|
|
4022
|
+
return new Promise((resolve) => {
|
|
4023
|
+
const escapedTitle = title.replace(/[;\x07\x1b\n\r]/g, "");
|
|
4024
|
+
const escapedMessage = message.replace(/[;\x07\x1b\n\r]/g, "");
|
|
4025
|
+
process.stdout.write(`\x1B]777;notify;${escapedTitle};${escapedMessage}\x07`, () => {
|
|
4026
|
+
resolve();
|
|
4027
|
+
});
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4012
4030
|
if (platform === "Darwin") {
|
|
4013
4031
|
if (notificationSystem === "node-notifier") {
|
|
4014
4032
|
return new Promise((resolve) => {
|
|
@@ -4180,18 +4198,20 @@ async function playSound(event, customPath, volume) {
|
|
|
4180
4198
|
|
|
4181
4199
|
// src/command.ts
|
|
4182
4200
|
import { spawn as spawn2 } from "child_process";
|
|
4183
|
-
function substituteTokens(value, event, message, sessionTitle, projectName) {
|
|
4201
|
+
function substituteTokens(value, event, message, sessionTitle, projectName, timestamp, turn) {
|
|
4184
4202
|
let result = value.replaceAll("{event}", event).replaceAll("{message}", message);
|
|
4185
4203
|
result = result.replaceAll("{sessionTitle}", sessionTitle || "");
|
|
4186
4204
|
result = result.replaceAll("{projectName}", projectName || "");
|
|
4205
|
+
result = result.replaceAll("{timestamp}", timestamp || "");
|
|
4206
|
+
result = result.replaceAll("{turn}", turn != null ? String(turn) : "");
|
|
4187
4207
|
return result;
|
|
4188
4208
|
}
|
|
4189
|
-
function runCommand2(config, event, message, sessionTitle, projectName) {
|
|
4209
|
+
function runCommand2(config, event, message, sessionTitle, projectName, timestamp, turn) {
|
|
4190
4210
|
if (!config.command.enabled || !config.command.path) {
|
|
4191
4211
|
return;
|
|
4192
4212
|
}
|
|
4193
|
-
const args = (config.command.args ?? []).map((arg) => substituteTokens(arg, event, message, sessionTitle, projectName));
|
|
4194
|
-
const command = substituteTokens(config.command.path, event, message, sessionTitle, projectName);
|
|
4213
|
+
const args = (config.command.args ?? []).map((arg) => substituteTokens(arg, event, message, sessionTitle, projectName, timestamp, turn));
|
|
4214
|
+
const command = substituteTokens(config.command.path, event, message, sessionTitle, projectName, timestamp, turn);
|
|
4195
4215
|
const proc = spawn2(command, args, {
|
|
4196
4216
|
stdio: "ignore",
|
|
4197
4217
|
detached: true
|
|
@@ -4451,6 +4471,30 @@ var pendingIdleTimers = new Map;
|
|
|
4451
4471
|
var sessionIdleSequence = new Map;
|
|
4452
4472
|
var sessionErrorSuppressionAt = new Map;
|
|
4453
4473
|
var sessionLastBusyAt = new Map;
|
|
4474
|
+
var globalTurnCount = null;
|
|
4475
|
+
function loadTurnCount() {
|
|
4476
|
+
try {
|
|
4477
|
+
const content = readFileSync3(getStatePath(), "utf-8");
|
|
4478
|
+
const state = JSON.parse(content);
|
|
4479
|
+
if (typeof state.turn === "number" && Number.isFinite(state.turn) && state.turn >= 0) {
|
|
4480
|
+
return state.turn;
|
|
4481
|
+
}
|
|
4482
|
+
} catch {}
|
|
4483
|
+
return 0;
|
|
4484
|
+
}
|
|
4485
|
+
function saveTurnCount(count) {
|
|
4486
|
+
try {
|
|
4487
|
+
writeFileSync2(getStatePath(), JSON.stringify({ turn: count }));
|
|
4488
|
+
} catch {}
|
|
4489
|
+
}
|
|
4490
|
+
function incrementTurnCount() {
|
|
4491
|
+
if (globalTurnCount === null) {
|
|
4492
|
+
globalTurnCount = loadTurnCount();
|
|
4493
|
+
}
|
|
4494
|
+
globalTurnCount++;
|
|
4495
|
+
saveTurnCount(globalTurnCount);
|
|
4496
|
+
return globalTurnCount;
|
|
4497
|
+
}
|
|
4454
4498
|
setInterval(() => {
|
|
4455
4499
|
const cutoff = Date.now() - 5 * 60 * 1000;
|
|
4456
4500
|
for (const [sessionID] of sessionIdleSequence) {
|
|
@@ -4475,15 +4519,26 @@ function getNotificationTitle(config, projectName) {
|
|
|
4475
4519
|
}
|
|
4476
4520
|
return "OpenCode";
|
|
4477
4521
|
}
|
|
4478
|
-
|
|
4522
|
+
function formatTimestamp() {
|
|
4523
|
+
const now = new Date;
|
|
4524
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
4525
|
+
const m = String(now.getMinutes()).padStart(2, "0");
|
|
4526
|
+
const s = String(now.getSeconds()).padStart(2, "0");
|
|
4527
|
+
return `${h}:${m}:${s}`;
|
|
4528
|
+
}
|
|
4529
|
+
async function handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle, sessionID) {
|
|
4479
4530
|
if (config.suppressWhenFocused && isTerminalFocused()) {
|
|
4480
4531
|
return;
|
|
4481
4532
|
}
|
|
4482
4533
|
const promises = [];
|
|
4534
|
+
const timestamp = formatTimestamp();
|
|
4535
|
+
const turn = incrementTurnCount();
|
|
4483
4536
|
const rawMessage = getMessage(config, eventType);
|
|
4484
4537
|
const message = interpolateMessage(rawMessage, {
|
|
4485
4538
|
sessionTitle: config.showSessionTitle ? sessionTitle : null,
|
|
4486
|
-
projectName
|
|
4539
|
+
projectName,
|
|
4540
|
+
timestamp,
|
|
4541
|
+
turn
|
|
4487
4542
|
});
|
|
4488
4543
|
if (isEventNotificationEnabled(config, eventType)) {
|
|
4489
4544
|
const title = getNotificationTitle(config, projectName);
|
|
@@ -4498,7 +4553,7 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds, sessi
|
|
|
4498
4553
|
const minDuration = config.command?.minDuration;
|
|
4499
4554
|
const shouldSkipCommand = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
|
|
4500
4555
|
if (!shouldSkipCommand) {
|
|
4501
|
-
runCommand2(config, eventType, message, sessionTitle, projectName);
|
|
4556
|
+
runCommand2(config, eventType, message, sessionTitle, projectName, timestamp, turn);
|
|
4502
4557
|
}
|
|
4503
4558
|
await Promise.allSettled(promises);
|
|
4504
4559
|
}
|
|
@@ -4632,7 +4687,7 @@ async function handleEventWithElapsedTime(client, config, eventType, projectName
|
|
|
4632
4687
|
const info = await getSessionInfo(client, sessionID);
|
|
4633
4688
|
sessionTitle = info.title;
|
|
4634
4689
|
}
|
|
4635
|
-
await handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle);
|
|
4690
|
+
await handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle, sessionID);
|
|
4636
4691
|
}
|
|
4637
4692
|
var NotifierPlugin = async ({ client, directory }) => {
|
|
4638
4693
|
const getConfig = () => loadConfig();
|
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.3",
|
|
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",
|