opencode-notifications 0.1.0
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/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/focus.d.ts +15 -0
- package/dist/focus.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +290 -0
- package/dist/notify.d.ts +40 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# opencode-notifications
|
|
2
|
+
|
|
3
|
+
Smart desktop notifications for [OpenCode](https://opencode.ai) - **only notifies when the terminal is not in focus**.
|
|
4
|
+
|
|
5
|
+
This is the key differentiator from other notification plugins: if you're already looking at OpenCode, you won't be bothered with redundant notifications.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Smart focus detection** - Only sends notifications when you're NOT looking at the terminal
|
|
10
|
+
- **X11 support** - Focus detection via `xdotool`
|
|
11
|
+
- **Tmux support** - Detects active tmux window/pane, works with multiple sessions
|
|
12
|
+
- **Configurable events** - Enable/disable notifications for specific event types
|
|
13
|
+
- **Lightweight** - No sound files, no complex dependencies
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add the plugin to your `opencode.json` or `opencode.jsonc`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"plugin": ["opencode-notifications"]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Restart OpenCode. The plugin will be automatically installed and loaded.
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
### X11
|
|
30
|
+
|
|
31
|
+
- `xdotool` - For window focus detection
|
|
32
|
+
- `notify-send` - For desktop notifications (usually from `libnotify-bin`)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Debian/Ubuntu
|
|
36
|
+
sudo apt install xdotool libnotify-bin
|
|
37
|
+
|
|
38
|
+
# Fedora
|
|
39
|
+
sudo dnf install xdotool libnotify
|
|
40
|
+
|
|
41
|
+
# Arch
|
|
42
|
+
sudo pacman -S xdotool libnotify
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Tmux
|
|
46
|
+
|
|
47
|
+
Tmux support works automatically when running OpenCode inside a tmux session. No additional setup is required.
|
|
48
|
+
|
|
49
|
+
When running inside tmux, notifications are suppressed only when **both**:
|
|
50
|
+
1. The terminal window is focused (X11 level)
|
|
51
|
+
2. The tmux window containing the OpenCode pane is active
|
|
52
|
+
|
|
53
|
+
This means you'll still receive notifications if:
|
|
54
|
+
- You switch to a different tmux window
|
|
55
|
+
- You're in a different tmux session
|
|
56
|
+
- The terminal itself is not focused
|
|
57
|
+
|
|
58
|
+
## Platform Support
|
|
59
|
+
|
|
60
|
+
Focus detection is supported on:
|
|
61
|
+
- Linux with X11
|
|
62
|
+
- Tmux (works with X11 for window and pane-level detection)
|
|
63
|
+
|
|
64
|
+
## Events
|
|
65
|
+
|
|
66
|
+
The plugin notifies on these OpenCode events:
|
|
67
|
+
|
|
68
|
+
| Event | Description |
|
|
69
|
+
|-------|-------------|
|
|
70
|
+
| `complete` | Generation/task completed (`session.idle`) |
|
|
71
|
+
| `error` | An error occurred (`session.error`) |
|
|
72
|
+
| `permission` | Permission needed (`permission.asked`) |
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
Create `~/.config/opencode/opencode-notifications.json` to customize:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"events": {
|
|
81
|
+
"complete": true,
|
|
82
|
+
"error": true,
|
|
83
|
+
"permission": true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Disable specific events
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"events": {
|
|
93
|
+
"complete": true,
|
|
94
|
+
"error": false,
|
|
95
|
+
"permission": true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## How It Works
|
|
101
|
+
|
|
102
|
+
1. When the plugin loads, it captures the window ID of the terminal running OpenCode
|
|
103
|
+
2. Before sending any notification, it checks if that window is still focused
|
|
104
|
+
3. If the terminal IS focused, the notification is skipped (you're already looking at it!)
|
|
105
|
+
4. If the terminal is NOT focused, the notification is sent
|
|
106
|
+
|
|
107
|
+
This simple approach ensures you're only notified when you need to be.
|
|
108
|
+
|
|
109
|
+
## Troubleshooting
|
|
110
|
+
|
|
111
|
+
### Notifications not appearing
|
|
112
|
+
|
|
113
|
+
1. **Check if `notify-send` is installed:**
|
|
114
|
+
```bash
|
|
115
|
+
notify-send "Test" "Hello"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
2. **Check your notification daemon:**
|
|
119
|
+
- GNOME: Notifications should work out of the box
|
|
120
|
+
- KDE: Notifications should work out of the box
|
|
121
|
+
|
|
122
|
+
### Focus detection not working
|
|
123
|
+
|
|
124
|
+
**X11 - Check if `xdotool` works:**
|
|
125
|
+
```bash
|
|
126
|
+
xdotool getactivewindow
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If this returns an error, make sure `xdotool` is installed.
|
|
130
|
+
|
|
131
|
+
### Notifications always showing (even when terminal is focused)
|
|
132
|
+
|
|
133
|
+
Make sure `xdotool` is installed and working on X11.
|
|
134
|
+
|
|
135
|
+
### Tmux: Notifications showing when pane is visible
|
|
136
|
+
|
|
137
|
+
If you're getting notifications even when the tmux pane running OpenCode is visible:
|
|
138
|
+
- Make sure the tmux window is **active** (not just visible in a split)
|
|
139
|
+
- Check that `TMUX_PANE` environment variable is set (it should be automatic)
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
144
|
+
|
|
145
|
+
## Contributing
|
|
146
|
+
|
|
147
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NotificationConfig } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Load and parse the configuration file
|
|
4
|
+
* Falls back to default config if file doesn't exist or is invalid
|
|
5
|
+
*/
|
|
6
|
+
export declare function loadConfig(): NotificationConfig;
|
|
7
|
+
/**
|
|
8
|
+
* Check if a specific event type is enabled
|
|
9
|
+
*/
|
|
10
|
+
export declare function isEventEnabled(config: NotificationConfig, eventType: "complete" | "error" | "permission" | "question"): boolean;
|
|
11
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AA6BjD;;;GAGG;AACH,wBAAgB,UAAU,IAAI,kBAAkB,CA2B/C;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,kBAAkB,EAC1B,SAAS,EAAE,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,GAC1D,OAAO,CAET"}
|
package/dist/focus.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FocusDetector } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Detect the display server and return the appropriate focus detector
|
|
4
|
+
* If running inside tmux, wraps the detector with tmux window-level checking
|
|
5
|
+
*/
|
|
6
|
+
export declare function createFocusDetector(): FocusDetector;
|
|
7
|
+
/**
|
|
8
|
+
* Initialize focus detection and return a function to check if terminal is focused
|
|
9
|
+
* Returns null if focus detection is not available
|
|
10
|
+
*/
|
|
11
|
+
export declare function initFocusDetection(): Promise<{
|
|
12
|
+
detector: FocusDetector;
|
|
13
|
+
terminalId: string | null;
|
|
14
|
+
}>;
|
|
15
|
+
//# sourceMappingURL=focus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAkH5C;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,aAAa,CAkBnD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC;IAClD,QAAQ,EAAE,aAAa,CAAA;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,CAAC,CAKD"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode Notifications Plugin
|
|
4
|
+
*
|
|
5
|
+
* Sends desktop notifications for meaningful events, but ONLY when the
|
|
6
|
+
* terminal running OpenCode is not in focus. This prevents unnecessary
|
|
7
|
+
* notifications when you're already looking at the terminal.
|
|
8
|
+
*
|
|
9
|
+
* Supports:
|
|
10
|
+
* - X11 (via xdotool)
|
|
11
|
+
* - Sway (via swaymsg)
|
|
12
|
+
* - Hyprland (via hyprctl)
|
|
13
|
+
* - Fallback for other Wayland compositors (always notify)
|
|
14
|
+
*/
|
|
15
|
+
export declare const NotificationsPlugin: Plugin;
|
|
16
|
+
export default NotificationsPlugin;
|
|
17
|
+
export type { NotificationConfig, EventType } from "./types";
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAWjD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,EAAE,MA+FjC,CAAA;AAGD,eAAe,mBAAmB,CAAA;AAGlC,YAAY,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
var DEFAULT_CONFIG = {
|
|
6
|
+
events: {
|
|
7
|
+
complete: true,
|
|
8
|
+
error: true,
|
|
9
|
+
permission: true,
|
|
10
|
+
question: true
|
|
11
|
+
},
|
|
12
|
+
sound: {
|
|
13
|
+
enabled: true
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var CONFIG_FILENAME = "opencode-notifications.json";
|
|
17
|
+
function getConfigPath() {
|
|
18
|
+
return join(homedir(), ".config", "opencode", CONFIG_FILENAME);
|
|
19
|
+
}
|
|
20
|
+
function loadConfig() {
|
|
21
|
+
const configPath = getConfigPath();
|
|
22
|
+
if (!existsSync(configPath)) {
|
|
23
|
+
return DEFAULT_CONFIG;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const content = readFileSync(configPath, "utf-8");
|
|
27
|
+
const parsed = JSON.parse(content);
|
|
28
|
+
return {
|
|
29
|
+
events: {
|
|
30
|
+
complete: parsed?.events?.complete ?? DEFAULT_CONFIG.events.complete,
|
|
31
|
+
error: parsed?.events?.error ?? DEFAULT_CONFIG.events.error,
|
|
32
|
+
permission: parsed?.events?.permission ?? DEFAULT_CONFIG.events.permission,
|
|
33
|
+
question: parsed?.events?.question ?? DEFAULT_CONFIG.events.question
|
|
34
|
+
},
|
|
35
|
+
sound: {
|
|
36
|
+
enabled: parsed?.sound?.enabled ?? DEFAULT_CONFIG.sound.enabled
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return DEFAULT_CONFIG;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isEventEnabled(config, eventType) {
|
|
44
|
+
return config.events[eventType];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/focus.ts
|
|
48
|
+
import { exec } from "child_process";
|
|
49
|
+
import { promisify } from "util";
|
|
50
|
+
var execAsync = promisify(exec);
|
|
51
|
+
async function runCommand(command) {
|
|
52
|
+
try {
|
|
53
|
+
const { stdout } = await execAsync(command, { timeout: 5000 });
|
|
54
|
+
return stdout.trim();
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function commandExists(cmd) {
|
|
60
|
+
const result = await runCommand(`which ${cmd}`);
|
|
61
|
+
return result !== null && result.length > 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class X11FocusDetector {
|
|
65
|
+
name = "X11 (xdotool)";
|
|
66
|
+
async init() {
|
|
67
|
+
if (!await commandExists("xdotool")) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return runCommand("xdotool getactivewindow");
|
|
71
|
+
}
|
|
72
|
+
async isTerminalFocused(terminalId) {
|
|
73
|
+
const currentWindow = await runCommand("xdotool getactivewindow");
|
|
74
|
+
return currentWindow === terminalId;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class TmuxFocusDetector {
|
|
79
|
+
name = "Tmux";
|
|
80
|
+
innerDetector;
|
|
81
|
+
paneId;
|
|
82
|
+
constructor(paneId, innerDetector) {
|
|
83
|
+
this.paneId = paneId;
|
|
84
|
+
this.innerDetector = innerDetector;
|
|
85
|
+
}
|
|
86
|
+
async init() {
|
|
87
|
+
if (!await commandExists("tmux")) {
|
|
88
|
+
return this.innerDetector.init();
|
|
89
|
+
}
|
|
90
|
+
return this.innerDetector.init();
|
|
91
|
+
}
|
|
92
|
+
async isTerminalFocused(terminalId) {
|
|
93
|
+
const terminalFocused = await this.innerDetector.isTerminalFocused(terminalId);
|
|
94
|
+
if (!terminalFocused) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const sessionAttached = await runCommand(`tmux display-message -t ${this.paneId} -p '#{session_attached}'`);
|
|
98
|
+
if (sessionAttached !== "1") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const windowActive = await runCommand(`tmux display-message -t ${this.paneId} -p '#{window_active}'`);
|
|
102
|
+
return windowActive === "1";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class AlwaysNotifyDetector {
|
|
107
|
+
name = "Fallback (always notify)";
|
|
108
|
+
async init() {
|
|
109
|
+
return "unsupported";
|
|
110
|
+
}
|
|
111
|
+
async isTerminalFocused(_terminalId) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function createFocusDetector() {
|
|
116
|
+
const sessionType = process.env.XDG_SESSION_TYPE;
|
|
117
|
+
const tmuxPane = process.env.TMUX_PANE;
|
|
118
|
+
let baseDetector;
|
|
119
|
+
if (sessionType === "x11" || process.env.DISPLAY) {
|
|
120
|
+
baseDetector = new X11FocusDetector;
|
|
121
|
+
} else {
|
|
122
|
+
baseDetector = new AlwaysNotifyDetector;
|
|
123
|
+
}
|
|
124
|
+
if (tmuxPane) {
|
|
125
|
+
return new TmuxFocusDetector(tmuxPane, baseDetector);
|
|
126
|
+
}
|
|
127
|
+
return baseDetector;
|
|
128
|
+
}
|
|
129
|
+
async function initFocusDetection() {
|
|
130
|
+
const detector = createFocusDetector();
|
|
131
|
+
const terminalId = await detector.init();
|
|
132
|
+
return { detector, terminalId };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/notify.ts
|
|
136
|
+
import { exec as exec2 } from "child_process";
|
|
137
|
+
import { promisify as promisify2 } from "util";
|
|
138
|
+
var execAsync2 = promisify2(exec2);
|
|
139
|
+
var NOTIFICATION_DEFAULTS = {
|
|
140
|
+
complete: {
|
|
141
|
+
title: "OpenCode Ready",
|
|
142
|
+
body: "Task completed",
|
|
143
|
+
icon: "dialog-information"
|
|
144
|
+
},
|
|
145
|
+
error: {
|
|
146
|
+
title: "OpenCode Error",
|
|
147
|
+
body: "An error occurred",
|
|
148
|
+
icon: "dialog-error"
|
|
149
|
+
},
|
|
150
|
+
permission: {
|
|
151
|
+
title: "OpenCode Permission",
|
|
152
|
+
body: "Action requires approval",
|
|
153
|
+
icon: "dialog-password"
|
|
154
|
+
},
|
|
155
|
+
question: {
|
|
156
|
+
title: "OpenCode Question",
|
|
157
|
+
body: "Your input is needed",
|
|
158
|
+
icon: "dialog-question"
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var EVENT_SOUNDS = {
|
|
162
|
+
complete: "dialog-information",
|
|
163
|
+
error: "dialog-error",
|
|
164
|
+
permission: "dialog-warning",
|
|
165
|
+
question: "dialog-question"
|
|
166
|
+
};
|
|
167
|
+
function getNotificationContent(eventType, context) {
|
|
168
|
+
const defaults = NOTIFICATION_DEFAULTS[eventType];
|
|
169
|
+
if (!context) {
|
|
170
|
+
return defaults;
|
|
171
|
+
}
|
|
172
|
+
let body = defaults.body;
|
|
173
|
+
switch (eventType) {
|
|
174
|
+
case "error":
|
|
175
|
+
if (context.errorMessage) {
|
|
176
|
+
body = truncate(context.errorMessage, 100);
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case "permission":
|
|
180
|
+
if (context.permissionName) {
|
|
181
|
+
const pattern = context.permissionPatterns?.[0];
|
|
182
|
+
body = pattern ? `${context.permissionName}: ${truncate(pattern, 60)}` : `${context.permissionName} requested`;
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
case "question":
|
|
186
|
+
if (context.questionText) {
|
|
187
|
+
body = truncate(context.questionText, 100);
|
|
188
|
+
} else if (context.questionHeader) {
|
|
189
|
+
body = truncate(context.questionHeader, 100);
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
...defaults,
|
|
195
|
+
body
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function getEventSound(eventType) {
|
|
199
|
+
return EVENT_SOUNDS[eventType];
|
|
200
|
+
}
|
|
201
|
+
async function playSound(soundId) {
|
|
202
|
+
try {
|
|
203
|
+
await execAsync2(`canberra-gtk-play -i "${soundId}"`, { timeout: 5000 });
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function sendNotification(title, body, options) {
|
|
208
|
+
const promises = [];
|
|
209
|
+
const iconArg = options?.icon ? `-i "${escapeShellArg(options.icon)}"` : "";
|
|
210
|
+
const cmd = `notify-send -a "OpenCode" -u normal ${iconArg} "${escapeShellArg(title)}" "${escapeShellArg(body)}"`;
|
|
211
|
+
promises.push(execAsync2(cmd, { timeout: 5000 }).then(() => {
|
|
212
|
+
}).catch(() => {
|
|
213
|
+
}));
|
|
214
|
+
if (options?.playSound && options?.soundId) {
|
|
215
|
+
promises.push(playSound(options.soundId));
|
|
216
|
+
}
|
|
217
|
+
await Promise.all(promises);
|
|
218
|
+
}
|
|
219
|
+
function escapeShellArg(arg) {
|
|
220
|
+
return arg.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
221
|
+
}
|
|
222
|
+
function truncate(str, maxLength) {
|
|
223
|
+
const cleaned = str.replace(/\s+/g, " ").trim();
|
|
224
|
+
if (cleaned.length <= maxLength) {
|
|
225
|
+
return cleaned;
|
|
226
|
+
}
|
|
227
|
+
return cleaned.slice(0, maxLength - 1) + "…";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/index.ts
|
|
231
|
+
var NotificationsPlugin = async () => {
|
|
232
|
+
const config = loadConfig();
|
|
233
|
+
const { detector, terminalId } = await initFocusDetection();
|
|
234
|
+
async function maybeNotify(eventType, context) {
|
|
235
|
+
if (!isEventEnabled(config, eventType)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (terminalId && await detector.isTerminalFocused(terminalId)) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const { title, body, icon } = getNotificationContent(eventType, context);
|
|
242
|
+
await sendNotification(title, body, {
|
|
243
|
+
icon,
|
|
244
|
+
playSound: config.sound.enabled,
|
|
245
|
+
soundId: getEventSound(eventType)
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
event: async ({ event }) => {
|
|
250
|
+
if (event.type === "session.idle") {
|
|
251
|
+
await maybeNotify("complete");
|
|
252
|
+
}
|
|
253
|
+
if (event.type === "session.error") {
|
|
254
|
+
const props = event.properties;
|
|
255
|
+
const errorMessage = props.error?.data?.message || props.error?.name || undefined;
|
|
256
|
+
await maybeNotify("error", { errorMessage });
|
|
257
|
+
}
|
|
258
|
+
if (event.type === "permission.updated") {
|
|
259
|
+
await maybeNotify("permission");
|
|
260
|
+
}
|
|
261
|
+
if (event.type === "permission.asked") {
|
|
262
|
+
const props = event.properties;
|
|
263
|
+
await maybeNotify("permission", {
|
|
264
|
+
permissionName: props.permission,
|
|
265
|
+
permissionPatterns: props.patterns
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (event.type === "question.asked") {
|
|
269
|
+
const props = event.properties;
|
|
270
|
+
const firstQuestion = props.questions?.[0];
|
|
271
|
+
await maybeNotify("question", {
|
|
272
|
+
questionText: firstQuestion?.question,
|
|
273
|
+
questionHeader: firstQuestion?.header
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
"permission.ask": async (input) => {
|
|
278
|
+
const props = input;
|
|
279
|
+
await maybeNotify("permission", {
|
|
280
|
+
permissionName: props.permission,
|
|
281
|
+
permissionPatterns: props.patterns
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
var src_default = NotificationsPlugin;
|
|
287
|
+
export {
|
|
288
|
+
src_default as default,
|
|
289
|
+
NotificationsPlugin
|
|
290
|
+
};
|
package/dist/notify.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { EventType, NotificationContent } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Event context passed from OpenCode events
|
|
4
|
+
*/
|
|
5
|
+
export interface EventContext {
|
|
6
|
+
/** Error message for error events */
|
|
7
|
+
errorMessage?: string;
|
|
8
|
+
/** Permission name for permission events */
|
|
9
|
+
permissionName?: string;
|
|
10
|
+
/** File patterns for permission events */
|
|
11
|
+
permissionPatterns?: string[];
|
|
12
|
+
/** Question text for question events */
|
|
13
|
+
questionText?: string;
|
|
14
|
+
/** Question header for question events */
|
|
15
|
+
questionHeader?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the notification content for an event type, optionally enhanced with context
|
|
19
|
+
*/
|
|
20
|
+
export declare function getNotificationContent(eventType: EventType, context?: EventContext): NotificationContent;
|
|
21
|
+
/**
|
|
22
|
+
* Get the sound ID for an event type
|
|
23
|
+
*/
|
|
24
|
+
export declare function getEventSound(eventType: EventType): string;
|
|
25
|
+
/**
|
|
26
|
+
* Send a desktop notification using notify-send
|
|
27
|
+
*
|
|
28
|
+
* @param title - Notification title
|
|
29
|
+
* @param body - Notification body text
|
|
30
|
+
* @param options - Additional options
|
|
31
|
+
* @param options.icon - Icon name (freedesktop icon theme)
|
|
32
|
+
* @param options.playSound - Whether to play a sound effect
|
|
33
|
+
* @param options.soundId - The freedesktop sound theme ID to play
|
|
34
|
+
*/
|
|
35
|
+
export declare function sendNotification(title: string, body: string, options?: {
|
|
36
|
+
icon?: string;
|
|
37
|
+
playSound?: boolean;
|
|
38
|
+
soundId?: string;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=notify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAyC7D;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,0CAA0C;IAC1C,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0CAA0C;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,SAAS,EACpB,OAAO,CAAC,EAAE,YAAY,GACrB,mBAAmB,CAwCrB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAE1D;AAgBD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACjE,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for notification events
|
|
3
|
+
*/
|
|
4
|
+
export interface EventsConfig {
|
|
5
|
+
/** Notify when generation completes (session.idle) */
|
|
6
|
+
complete: boolean;
|
|
7
|
+
/** Notify when an error occurs (session.error) */
|
|
8
|
+
error: boolean;
|
|
9
|
+
/** Notify when permission is needed (permission.asked) */
|
|
10
|
+
permission: boolean;
|
|
11
|
+
/** Notify when a question is asked (question.asked) */
|
|
12
|
+
question: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sound configuration
|
|
16
|
+
*/
|
|
17
|
+
export interface SoundConfig {
|
|
18
|
+
/** Enable sound effects for notifications */
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Plugin configuration
|
|
23
|
+
*/
|
|
24
|
+
export interface NotificationConfig {
|
|
25
|
+
events: EventsConfig;
|
|
26
|
+
sound: SoundConfig;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Event types that trigger notifications
|
|
30
|
+
*/
|
|
31
|
+
export type EventType = "complete" | "error" | "permission" | "question";
|
|
32
|
+
/**
|
|
33
|
+
* Notification content
|
|
34
|
+
*/
|
|
35
|
+
export interface NotificationContent {
|
|
36
|
+
title: string;
|
|
37
|
+
body: string;
|
|
38
|
+
icon: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Focus detector interface for different display servers
|
|
42
|
+
*/
|
|
43
|
+
export interface FocusDetector {
|
|
44
|
+
/** Get the name of the display server/compositor */
|
|
45
|
+
readonly name: string;
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the detector and capture the terminal window ID
|
|
48
|
+
* @returns The terminal window/container ID, or null if detection is not supported
|
|
49
|
+
*/
|
|
50
|
+
init(): Promise<string | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Check if the terminal window is currently focused
|
|
53
|
+
* @param terminalId The terminal ID captured during init
|
|
54
|
+
* @returns true if the terminal is focused, false otherwise
|
|
55
|
+
*/
|
|
56
|
+
isTerminalFocused(terminalId: string): Promise<boolean>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Result of running a shell command
|
|
60
|
+
*/
|
|
61
|
+
export interface CommandResult {
|
|
62
|
+
stdout: string;
|
|
63
|
+
stderr: string;
|
|
64
|
+
exitCode: number;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,sDAAsD;IACtD,QAAQ,EAAE,OAAO,CAAA;IACjB,kDAAkD;IAClD,KAAK,EAAE,OAAO,CAAA;IACd,0DAA0D;IAC1D,UAAU,EAAE,OAAO,CAAA;IACnB,uDAAuD;IACvD,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,6CAA6C;IAC7C,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,YAAY,CAAA;IACpB,KAAK,EAAE,WAAW,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,CAAA;AAExE;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IAErB;;;OAGG;IACH,IAAI,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAE9B;;;;OAIG;IACH,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACxD;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;CACjB"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-notifications",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Smart desktop notifications for OpenCode - only notifies when terminal is not focused (Linux)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node && tsc --emitDeclarationOnly --declaration --outDir ./dist",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"prepublishOnly": "bun run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"opencode",
|
|
17
|
+
"plugin",
|
|
18
|
+
"notifications",
|
|
19
|
+
"linux",
|
|
20
|
+
"desktop",
|
|
21
|
+
"x11"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/YOUR_USERNAME/opencode-notifications.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/YOUR_USERNAME/opencode-notifications/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/YOUR_USERNAME/opencode-notifications#readme",
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@opencode-ai/plugin": ">=0.0.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@opencode-ai/plugin": "latest",
|
|
38
|
+
"@types/bun": "latest",
|
|
39
|
+
"typescript": "^5.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|