@mohak34/opencode-notifier 0.1.32 → 0.1.34-beta
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 +19 -13
- package/dist/index.js +40 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
|
|
|
55
55
|
"showSessionTitle": false,
|
|
56
56
|
"showIcon": true,
|
|
57
57
|
"suppressWhenFocused": true,
|
|
58
|
+
"enableOnDesktop": false,
|
|
58
59
|
"notificationSystem": "osascript",
|
|
59
60
|
"linux": {
|
|
60
61
|
"grouping": false
|
|
@@ -66,12 +67,12 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
|
|
|
66
67
|
"minDuration": 0
|
|
67
68
|
},
|
|
68
69
|
"events": {
|
|
69
|
-
"permission": { "sound": true, "notification": true },
|
|
70
|
-
"complete": { "sound": true, "notification": true },
|
|
71
|
-
"subagent_complete": { "sound": false, "notification": false },
|
|
72
|
-
"error": { "sound": true, "notification": true },
|
|
73
|
-
"question": { "sound": true, "notification": true },
|
|
74
|
-
"user_cancelled": { "sound": false, "notification": false }
|
|
70
|
+
"permission": { "sound": true, "notification": true, "command": true },
|
|
71
|
+
"complete": { "sound": true, "notification": true, "command": true },
|
|
72
|
+
"subagent_complete": { "sound": false, "notification": false, "command": true },
|
|
73
|
+
"error": { "sound": true, "notification": true, "command": true },
|
|
74
|
+
"question": { "sound": true, "notification": true, "command": true },
|
|
75
|
+
"user_cancelled": { "sound": false, "notification": false, "command": true }
|
|
75
76
|
},
|
|
76
77
|
"messages": {
|
|
77
78
|
"permission": "Session needs permission: {sessionTitle}",
|
|
@@ -112,6 +113,8 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
|
|
|
112
113
|
"showProjectName": true,
|
|
113
114
|
"showSessionTitle": false,
|
|
114
115
|
"showIcon": true,
|
|
116
|
+
"suppressWhenFocused": true,
|
|
117
|
+
"enableOnDesktop": false,
|
|
115
118
|
"notificationSystem": "osascript"
|
|
116
119
|
}
|
|
117
120
|
```
|
|
@@ -123,7 +126,8 @@ Create `~/.config/opencode/opencode-notifier.json` with the defaults:
|
|
|
123
126
|
- `showSessionTitle` - Include the session title in notification messages via `{sessionTitle}` placeholder (default: true)
|
|
124
127
|
- `showIcon` - Show OpenCode icon, Windows/Linux only (default: true)
|
|
125
128
|
- `suppressWhenFocused` - Skip notifications and sounds when the terminal is the active window (default: true). See [Focus detection](#focus-detection) for platform details
|
|
126
|
-
- `
|
|
129
|
+
- `enableOnDesktop` - Run the plugin on Desktop and Web clients (default: false). When false, the plugin only runs on CLI. Set to true if you want notifications/sounds/commands on Desktop/Web — useful if you want custom commands (Telegram, webhooks) but don't care about built-in notifications
|
|
130
|
+
- `notificationSystem` - macOS only: `"osascript"`, `"node-notifier"`, or `"ghostty"` (default: "osascript"). Use `"ghostty"` if you're running Ghostty terminal for native OSC 9 notifications
|
|
127
131
|
- `linux.grouping` - Linux only: replace notifications in-place instead of stacking (default: false). Requires `notify-send` 0.8+
|
|
128
132
|
|
|
129
133
|
### Events
|
|
@@ -133,18 +137,20 @@ Control each event separately:
|
|
|
133
137
|
```json
|
|
134
138
|
{
|
|
135
139
|
"events": {
|
|
136
|
-
"permission": { "sound": true, "notification": true },
|
|
137
|
-
"complete": { "sound": true, "notification": true },
|
|
138
|
-
"subagent_complete": { "sound": false, "notification": false },
|
|
139
|
-
"error": { "sound": true, "notification": true },
|
|
140
|
-
"question": { "sound": true, "notification": true },
|
|
141
|
-
"user_cancelled": { "sound": false, "notification": false }
|
|
140
|
+
"permission": { "sound": true, "notification": true, "command": true },
|
|
141
|
+
"complete": { "sound": true, "notification": true, "command": true },
|
|
142
|
+
"subagent_complete": { "sound": false, "notification": false, "command": true },
|
|
143
|
+
"error": { "sound": true, "notification": true, "command": true },
|
|
144
|
+
"question": { "sound": true, "notification": true, "command": true },
|
|
145
|
+
"user_cancelled": { "sound": false, "notification": false, "command": true }
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
```
|
|
145
149
|
|
|
146
150
|
`user_cancelled` fires when you press ESC to abort a session. It's silent by default so intentional cancellations don't trigger error alerts. Set `sound` or `notification` to `true` if you want confirmation when cancelling.
|
|
147
151
|
|
|
152
|
+
The `command` property controls whether the custom command (see [Custom commands](#custom-commands)) runs for that event. Defaults to `true` for all events. Set it to `false` to suppress the command for specific events without disabling it globally.
|
|
153
|
+
|
|
148
154
|
Or use true/false for both:
|
|
149
155
|
|
|
150
156
|
```json
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { basename } from "path";
|
|
3
|
-
import { readFileSync as readFileSync2, writeFileSync
|
|
3
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
4
4
|
|
|
5
5
|
// src/config.ts
|
|
6
6
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -9,7 +9,8 @@ import { homedir } from "os";
|
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
var DEFAULT_EVENT_CONFIG = {
|
|
11
11
|
sound: true,
|
|
12
|
-
notification: true
|
|
12
|
+
notification: true,
|
|
13
|
+
command: true
|
|
13
14
|
};
|
|
14
15
|
var DEFAULT_CONFIG = {
|
|
15
16
|
sound: true,
|
|
@@ -19,6 +20,7 @@ var DEFAULT_CONFIG = {
|
|
|
19
20
|
showSessionTitle: false,
|
|
20
21
|
showIcon: true,
|
|
21
22
|
suppressWhenFocused: true,
|
|
23
|
+
enableOnDesktop: false,
|
|
22
24
|
notificationSystem: "osascript",
|
|
23
25
|
linux: {
|
|
24
26
|
grouping: false
|
|
@@ -31,11 +33,11 @@ var DEFAULT_CONFIG = {
|
|
|
31
33
|
events: {
|
|
32
34
|
permission: { ...DEFAULT_EVENT_CONFIG },
|
|
33
35
|
complete: { ...DEFAULT_EVENT_CONFIG },
|
|
34
|
-
subagent_complete: { sound: false, notification: false },
|
|
36
|
+
subagent_complete: { ...DEFAULT_EVENT_CONFIG, sound: false, notification: false },
|
|
35
37
|
error: { ...DEFAULT_EVENT_CONFIG },
|
|
36
38
|
question: { ...DEFAULT_EVENT_CONFIG },
|
|
37
39
|
interrupted: { ...DEFAULT_EVENT_CONFIG },
|
|
38
|
-
user_cancelled: { sound: false, notification: false }
|
|
40
|
+
user_cancelled: { ...DEFAULT_EVENT_CONFIG, sound: false, notification: false }
|
|
39
41
|
},
|
|
40
42
|
messages: {
|
|
41
43
|
permission: "Session needs permission: {sessionTitle}",
|
|
@@ -82,12 +84,14 @@ function parseEventConfig(userEvent, defaultConfig) {
|
|
|
82
84
|
if (typeof userEvent === "boolean") {
|
|
83
85
|
return {
|
|
84
86
|
sound: userEvent,
|
|
85
|
-
notification: userEvent
|
|
87
|
+
notification: userEvent,
|
|
88
|
+
command: userEvent
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
91
|
return {
|
|
89
92
|
sound: userEvent.sound ?? defaultConfig.sound,
|
|
90
|
-
notification: userEvent.notification ?? defaultConfig.notification
|
|
93
|
+
notification: userEvent.notification ?? defaultConfig.notification,
|
|
94
|
+
command: userEvent.command ?? defaultConfig.command
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
97
|
function parseVolume(value, defaultVolume) {
|
|
@@ -114,7 +118,8 @@ function loadConfig() {
|
|
|
114
118
|
const globalNotification = userConfig.notification ?? DEFAULT_CONFIG.notification;
|
|
115
119
|
const defaultWithGlobal = {
|
|
116
120
|
sound: globalSound,
|
|
117
|
-
notification: globalNotification
|
|
121
|
+
notification: globalNotification,
|
|
122
|
+
command: true
|
|
118
123
|
};
|
|
119
124
|
const userCommand = userConfig.command ?? {};
|
|
120
125
|
const commandArgs = Array.isArray(userCommand.args) ? userCommand.args.filter((arg) => typeof arg === "string") : undefined;
|
|
@@ -127,6 +132,7 @@ function loadConfig() {
|
|
|
127
132
|
showSessionTitle: userConfig.showSessionTitle ?? DEFAULT_CONFIG.showSessionTitle,
|
|
128
133
|
showIcon: userConfig.showIcon ?? DEFAULT_CONFIG.showIcon,
|
|
129
134
|
suppressWhenFocused: userConfig.suppressWhenFocused ?? DEFAULT_CONFIG.suppressWhenFocused,
|
|
135
|
+
enableOnDesktop: typeof userConfig.enableOnDesktop === "boolean" ? userConfig.enableOnDesktop : DEFAULT_CONFIG.enableOnDesktop,
|
|
130
136
|
notificationSystem: userConfig.notificationSystem === "node-notifier" ? "node-notifier" : userConfig.notificationSystem === "ghostty" ? "ghostty" : "osascript",
|
|
131
137
|
linux: {
|
|
132
138
|
grouping: typeof userConfig.linux?.grouping === "boolean" ? userConfig.linux.grouping : DEFAULT_CONFIG.linux.grouping
|
|
@@ -140,11 +146,11 @@ function loadConfig() {
|
|
|
140
146
|
events: {
|
|
141
147
|
permission: parseEventConfig(userConfig.events?.permission ?? userConfig.permission, defaultWithGlobal),
|
|
142
148
|
complete: parseEventConfig(userConfig.events?.complete ?? userConfig.complete, defaultWithGlobal),
|
|
143
|
-
subagent_complete: parseEventConfig(userConfig.events?.subagent_complete ?? userConfig.subagent_complete, { sound: false, notification: false }),
|
|
149
|
+
subagent_complete: parseEventConfig(userConfig.events?.subagent_complete ?? userConfig.subagent_complete, { sound: false, notification: false, command: true }),
|
|
144
150
|
error: parseEventConfig(userConfig.events?.error ?? userConfig.error, defaultWithGlobal),
|
|
145
151
|
question: parseEventConfig(userConfig.events?.question ?? userConfig.question, defaultWithGlobal),
|
|
146
152
|
interrupted: parseEventConfig(userConfig.events?.interrupted ?? userConfig.interrupted, defaultWithGlobal),
|
|
147
|
-
user_cancelled: parseEventConfig(userConfig.events?.user_cancelled ?? userConfig.user_cancelled, { sound: false, notification: false })
|
|
153
|
+
user_cancelled: parseEventConfig(userConfig.events?.user_cancelled ?? userConfig.user_cancelled, { sound: false, notification: false, command: true })
|
|
148
154
|
},
|
|
149
155
|
messages: {
|
|
150
156
|
permission: userConfig.messages?.permission ?? DEFAULT_CONFIG.messages.permission,
|
|
@@ -184,6 +190,9 @@ function isEventSoundEnabled(config, event) {
|
|
|
184
190
|
function isEventNotificationEnabled(config, event) {
|
|
185
191
|
return config.events[event].notification;
|
|
186
192
|
}
|
|
193
|
+
function isEventCommandEnabled(config, event) {
|
|
194
|
+
return config.events[event].command;
|
|
195
|
+
}
|
|
187
196
|
function getMessage(config, event) {
|
|
188
197
|
return config.messages[event];
|
|
189
198
|
}
|
|
@@ -262,6 +271,7 @@ function detectNotifySendCapabilities() {
|
|
|
262
271
|
function sendLinuxNotificationDirect(title, message, timeout, iconPath, grouping = true) {
|
|
263
272
|
return new Promise((resolve) => {
|
|
264
273
|
const args = [];
|
|
274
|
+
args.push("--app-name", "opencode");
|
|
265
275
|
if (iconPath) {
|
|
266
276
|
args.push("--icon", iconPath);
|
|
267
277
|
}
|
|
@@ -336,7 +346,8 @@ async function sendNotification(title, message, timeout, iconPath, notificationS
|
|
|
336
346
|
title,
|
|
337
347
|
message,
|
|
338
348
|
timeout,
|
|
339
|
-
icon: iconPath
|
|
349
|
+
icon: iconPath,
|
|
350
|
+
"app-name": "opencode"
|
|
340
351
|
};
|
|
341
352
|
platformNotifier.notify(notificationOptions, () => {
|
|
342
353
|
resolve();
|
|
@@ -420,7 +431,7 @@ async function playOnLinux(soundPath, volume) {
|
|
|
420
431
|
const players = [
|
|
421
432
|
{ command: "paplay", args: [`--volume=${pulseVolume}`, soundPath] },
|
|
422
433
|
{ command: "aplay", args: [soundPath] },
|
|
423
|
-
{ command: "mpv", args: ["--no-video", "--no-terminal", `--volume=${percentVolume}`, soundPath] },
|
|
434
|
+
{ command: "mpv", args: ["--no-video", "--no-terminal", "--script-opts=autoload-disabled=yes", `--volume=${percentVolume}`, soundPath] },
|
|
424
435
|
{ command: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", "-volume", `${percentVolume}`, soundPath] }
|
|
425
436
|
];
|
|
426
437
|
for (const player of players) {
|
|
@@ -493,7 +504,7 @@ function runCommand2(config, event, message, sessionTitle, projectName, timestam
|
|
|
493
504
|
}
|
|
494
505
|
|
|
495
506
|
// src/focus.ts
|
|
496
|
-
import { execSync } from "child_process";
|
|
507
|
+
import { execFileSync, execSync } from "child_process";
|
|
497
508
|
function execWithTimeout(command, timeoutMs = 500) {
|
|
498
509
|
try {
|
|
499
510
|
return execSync(command, { timeout: timeoutMs, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -501,6 +512,13 @@ function execWithTimeout(command, timeoutMs = 500) {
|
|
|
501
512
|
return null;
|
|
502
513
|
}
|
|
503
514
|
}
|
|
515
|
+
function execFileWithTimeout(command, args, timeoutMs = 500) {
|
|
516
|
+
try {
|
|
517
|
+
return execFileSync(command, args, { timeout: timeoutMs, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
518
|
+
} catch {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
504
522
|
function getHyprlandActiveWindowId() {
|
|
505
523
|
const output = execWithTimeout("hyprctl activewindow -j");
|
|
506
524
|
if (!output)
|
|
@@ -554,18 +572,11 @@ function getLinuxWaylandActiveWindowId() {
|
|
|
554
572
|
return null;
|
|
555
573
|
}
|
|
556
574
|
function getWindowsActiveWindowId() {
|
|
557
|
-
const script =
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
563
|
-
}
|
|
564
|
-
"@
|
|
565
|
-
$hwnd = [FocusHelper]::GetForegroundWindow()
|
|
566
|
-
Write-Output $hwnd
|
|
567
|
-
`.trim().replace(/\n/g, "; ");
|
|
568
|
-
return execWithTimeout(`powershell -NoProfile -Command "${script}"`, 1000);
|
|
575
|
+
const script = `$type=Add-Type -Name FocusHelper -Namespace OpenCodeNotifier -MemberDefinition '[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();' -PassThru; $type::GetForegroundWindow()`;
|
|
576
|
+
let windowId = execFileWithTimeout("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], 1000);
|
|
577
|
+
if (!windowId)
|
|
578
|
+
windowId = execFileWithTimeout("pwsh", ["-NoProfile", "-NonInteractive", "-Command", script], 1000);
|
|
579
|
+
return windowId;
|
|
569
580
|
}
|
|
570
581
|
function getMacOSActiveWindowId() {
|
|
571
582
|
return execWithTimeout(`osascript -e 'tell application "System Events" to return id of window 1 of (first application process whose frontmost is true)'`);
|
|
@@ -630,7 +641,7 @@ function loadTurnCount() {
|
|
|
630
641
|
}
|
|
631
642
|
function saveTurnCount(count) {
|
|
632
643
|
try {
|
|
633
|
-
|
|
644
|
+
writeFileSync(getStatePath(), JSON.stringify({ turn: count }));
|
|
634
645
|
} catch {}
|
|
635
646
|
}
|
|
636
647
|
function incrementTurnCount() {
|
|
@@ -697,7 +708,7 @@ async function handleEvent(config, eventType, projectName, elapsedSeconds, sessi
|
|
|
697
708
|
promises.push(playSound(eventType, customSoundPath, soundVolume));
|
|
698
709
|
}
|
|
699
710
|
const minDuration = config.command?.minDuration;
|
|
700
|
-
const shouldSkipCommand = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
|
|
711
|
+
const shouldSkipCommand = !isEventCommandEnabled(config, eventType) || typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration > 0 && typeof elapsedSeconds === "number" && Number.isFinite(elapsedSeconds) && elapsedSeconds < minDuration;
|
|
701
712
|
if (!shouldSkipCommand) {
|
|
702
713
|
runCommand2(config, eventType, message, sessionTitle, projectName, timestamp, turn);
|
|
703
714
|
}
|
|
@@ -838,7 +849,9 @@ async function handleEventWithElapsedTime(client, config, eventType, projectName
|
|
|
838
849
|
var NotifierPlugin = async ({ client, directory }) => {
|
|
839
850
|
const clientEnv = process.env.OPENCODE_CLIENT;
|
|
840
851
|
if (clientEnv && clientEnv !== "cli") {
|
|
841
|
-
|
|
852
|
+
const config = loadConfig();
|
|
853
|
+
if (!config.enableOnDesktop)
|
|
854
|
+
return {};
|
|
842
855
|
}
|
|
843
856
|
const getConfig = () => loadConfig();
|
|
844
857
|
const projectName = directory ? basename(directory) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mohak34/opencode-notifier",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34-beta",
|
|
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",
|