@mohak34/opencode-notifier 0.1.30-beta.1 → 0.1.30-beta.2
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 +48 -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
|
@@ -3850,7 +3850,7 @@ function loadConfig() {
|
|
|
3850
3850
|
showSessionTitle: userConfig.showSessionTitle ?? DEFAULT_CONFIG.showSessionTitle,
|
|
3851
3851
|
showIcon: userConfig.showIcon ?? DEFAULT_CONFIG.showIcon,
|
|
3852
3852
|
suppressWhenFocused: userConfig.suppressWhenFocused ?? DEFAULT_CONFIG.suppressWhenFocused,
|
|
3853
|
-
notificationSystem: userConfig.notificationSystem === "node-notifier" ? "node-notifier" : "osascript",
|
|
3853
|
+
notificationSystem: userConfig.notificationSystem === "node-notifier" ? "node-notifier" : userConfig.notificationSystem === "ghostty" ? "ghostty" : "osascript",
|
|
3854
3854
|
linux: {
|
|
3855
3855
|
grouping: typeof userConfig.linux?.grouping === "boolean" ? userConfig.linux.grouping : DEFAULT_CONFIG.linux.grouping
|
|
3856
3856
|
},
|
|
@@ -3936,6 +3936,10 @@ function interpolateMessage(message, context) {
|
|
|
3936
3936
|
result = result.replaceAll("{sessionTitle}", sessionTitle);
|
|
3937
3937
|
const projectName = context.projectName || "";
|
|
3938
3938
|
result = result.replaceAll("{projectName}", projectName);
|
|
3939
|
+
const timestamp = context.timestamp || "";
|
|
3940
|
+
result = result.replaceAll("{timestamp}", timestamp);
|
|
3941
|
+
const turn = context.turn != null ? String(context.turn) : "";
|
|
3942
|
+
result = result.replaceAll("{turn}", turn);
|
|
3939
3943
|
result = result.replace(/\s*[:\-|]\s*$/, "").trim();
|
|
3940
3944
|
result = result.replace(/\s{2,}/g, " ");
|
|
3941
3945
|
return result;
|
|
@@ -4009,6 +4013,15 @@ async function sendNotification(title, message, timeout, iconPath, notificationS
|
|
|
4009
4013
|
return;
|
|
4010
4014
|
}
|
|
4011
4015
|
lastNotificationTime[message] = now;
|
|
4016
|
+
if (notificationSystem === "ghostty") {
|
|
4017
|
+
return new Promise((resolve) => {
|
|
4018
|
+
const escapedTitle = title.replace(/[;\x07\x1b\n\r]/g, "");
|
|
4019
|
+
const escapedMessage = message.replace(/[;\x07\x1b\n\r]/g, "");
|
|
4020
|
+
process.stdout.write(`\x1B]777;notify;${escapedTitle};${escapedMessage}\x07`, () => {
|
|
4021
|
+
resolve();
|
|
4022
|
+
});
|
|
4023
|
+
});
|
|
4024
|
+
}
|
|
4012
4025
|
if (platform === "Darwin") {
|
|
4013
4026
|
if (notificationSystem === "node-notifier") {
|
|
4014
4027
|
return new Promise((resolve) => {
|
|
@@ -4180,18 +4193,20 @@ async function playSound(event, customPath, volume) {
|
|
|
4180
4193
|
|
|
4181
4194
|
// src/command.ts
|
|
4182
4195
|
import { spawn as spawn2 } from "child_process";
|
|
4183
|
-
function substituteTokens(value, event, message, sessionTitle, projectName) {
|
|
4196
|
+
function substituteTokens(value, event, message, sessionTitle, projectName, timestamp, turn) {
|
|
4184
4197
|
let result = value.replaceAll("{event}", event).replaceAll("{message}", message);
|
|
4185
4198
|
result = result.replaceAll("{sessionTitle}", sessionTitle || "");
|
|
4186
4199
|
result = result.replaceAll("{projectName}", projectName || "");
|
|
4200
|
+
result = result.replaceAll("{timestamp}", timestamp || "");
|
|
4201
|
+
result = result.replaceAll("{turn}", turn != null ? String(turn) : "");
|
|
4187
4202
|
return result;
|
|
4188
4203
|
}
|
|
4189
|
-
function runCommand2(config, event, message, sessionTitle, projectName) {
|
|
4204
|
+
function runCommand2(config, event, message, sessionTitle, projectName, timestamp, turn) {
|
|
4190
4205
|
if (!config.command.enabled || !config.command.path) {
|
|
4191
4206
|
return;
|
|
4192
4207
|
}
|
|
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);
|
|
4208
|
+
const args = (config.command.args ?? []).map((arg) => substituteTokens(arg, event, message, sessionTitle, projectName, timestamp, turn));
|
|
4209
|
+
const command = substituteTokens(config.command.path, event, message, sessionTitle, projectName, timestamp, turn);
|
|
4195
4210
|
const proc = spawn2(command, args, {
|
|
4196
4211
|
stdio: "ignore",
|
|
4197
4212
|
detached: true
|
|
@@ -4451,6 +4466,7 @@ var pendingIdleTimers = new Map;
|
|
|
4451
4466
|
var sessionIdleSequence = new Map;
|
|
4452
4467
|
var sessionErrorSuppressionAt = new Map;
|
|
4453
4468
|
var sessionLastBusyAt = new Map;
|
|
4469
|
+
var sessionTurnCount = new Map;
|
|
4454
4470
|
setInterval(() => {
|
|
4455
4471
|
const cutoff = Date.now() - 5 * 60 * 1000;
|
|
4456
4472
|
for (const [sessionID] of sessionIdleSequence) {
|
|
@@ -4468,6 +4484,11 @@ setInterval(() => {
|
|
|
4468
4484
|
sessionLastBusyAt.delete(sessionID);
|
|
4469
4485
|
}
|
|
4470
4486
|
}
|
|
4487
|
+
for (const [sessionID] of sessionTurnCount) {
|
|
4488
|
+
if (!pendingIdleTimers.has(sessionID) && !sessionLastBusyAt.has(sessionID)) {
|
|
4489
|
+
sessionTurnCount.delete(sessionID);
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4471
4492
|
}, 5 * 60 * 1000);
|
|
4472
4493
|
function getNotificationTitle(config, projectName) {
|
|
4473
4494
|
if (config.showProjectName && projectName) {
|
|
@@ -4475,15 +4496,33 @@ function getNotificationTitle(config, projectName) {
|
|
|
4475
4496
|
}
|
|
4476
4497
|
return "OpenCode";
|
|
4477
4498
|
}
|
|
4478
|
-
|
|
4499
|
+
function formatTimestamp() {
|
|
4500
|
+
const now = new Date;
|
|
4501
|
+
const h = String(now.getHours()).padStart(2, "0");
|
|
4502
|
+
const m = String(now.getMinutes()).padStart(2, "0");
|
|
4503
|
+
const s = String(now.getSeconds()).padStart(2, "0");
|
|
4504
|
+
return `${h}:${m}:${s}`;
|
|
4505
|
+
}
|
|
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
|
+
async function handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle, sessionID) {
|
|
4479
4514
|
if (config.suppressWhenFocused && isTerminalFocused()) {
|
|
4480
4515
|
return;
|
|
4481
4516
|
}
|
|
4482
4517
|
const promises = [];
|
|
4518
|
+
const timestamp = formatTimestamp();
|
|
4519
|
+
const turn = incrementSessionTurn(sessionID ?? null);
|
|
4483
4520
|
const rawMessage = getMessage(config, eventType);
|
|
4484
4521
|
const message = interpolateMessage(rawMessage, {
|
|
4485
4522
|
sessionTitle: config.showSessionTitle ? sessionTitle : null,
|
|
4486
|
-
projectName
|
|
4523
|
+
projectName,
|
|
4524
|
+
timestamp,
|
|
4525
|
+
turn
|
|
4487
4526
|
});
|
|
4488
4527
|
if (isEventNotificationEnabled(config, eventType)) {
|
|
4489
4528
|
const title = getNotificationTitle(config, projectName);
|
|
@@ -4498,7 +4537,7 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds, sessi
|
|
|
4498
4537
|
const minDuration = config.command?.minDuration;
|
|
4499
4538
|
const shouldSkipCommand = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
|
|
4500
4539
|
if (!shouldSkipCommand) {
|
|
4501
|
-
runCommand2(config, eventType, message, sessionTitle, projectName);
|
|
4540
|
+
runCommand2(config, eventType, message, sessionTitle, projectName, timestamp, turn);
|
|
4502
4541
|
}
|
|
4503
4542
|
await Promise.allSettled(promises);
|
|
4504
4543
|
}
|
|
@@ -4632,7 +4671,7 @@ async function handleEventWithElapsedTime(client, config, eventType, projectName
|
|
|
4632
4671
|
const info = await getSessionInfo(client, sessionID);
|
|
4633
4672
|
sessionTitle = info.title;
|
|
4634
4673
|
}
|
|
4635
|
-
await handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle);
|
|
4674
|
+
await handleEvent(config, eventType, projectName, elapsedSeconds, sessionTitle, sessionID);
|
|
4636
4675
|
}
|
|
4637
4676
|
var NotifierPlugin = async ({ client, directory }) => {
|
|
4638
4677
|
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.2",
|
|
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",
|