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 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.
@@ -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"}
@@ -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"}
@@ -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
+ };
@@ -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"}
@@ -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
+ }