opencode-probleemwijken 1.1.0 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +61 -16
  2. package/dist/index.js +157 -23
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # opencode-probleemwijken
2
2
 
3
- OpenCode plugin dat een willekeurig geluid afspeelt van de legendarische [Probleemwijken/Derkolk soundboard](https://www.derkolk.nl/probleemwijken/) wanneer een sessie klaar is.
3
+ OpenCode plugin dat een willekeurig geluid afspeelt en push notificaties stuurt van de legendarische [Probleemwijken/Derkolk soundboard](https://www.derkolk.nl/probleemwijken/) wanneer een sessie klaar is.
4
4
 
5
5
  ## Installatie
6
6
 
@@ -24,7 +24,9 @@ Herstart OpenCode en je bent klaar!
24
24
 
25
25
  ## Wat doet het?
26
26
 
27
- Elke keer als OpenCode klaar is met een taak (`session.idle`) of een error krijgt (`session.error`), speelt de plugin een willekeurig geluid af uit de collectie van 36 klassieke Derkolk soundboard fragmenten.
27
+ Elke keer als OpenCode klaar is met een taak (`session.idle`) of een error krijgt (`session.error`):
28
+ - Speelt een willekeurig geluid af uit de collectie van 36 klassieke Derkolk soundboard fragmenten
29
+ - Stuurt een push notificatie naar je desktop
28
30
 
29
31
  ## Geluiden
30
32
 
@@ -42,11 +44,11 @@ Elke keer als OpenCode klaar is met een taak (`session.idle`) of een error krijg
42
44
 
43
45
  ## Platform ondersteuning
44
46
 
45
- | Platform | Audio player |
46
- |----------|--------------|
47
- | macOS | `afplay` (ingebouwd) |
48
- | Linux | `mpv` of `ffplay` |
49
- | Windows | Windows Media Player via PowerShell |
47
+ | Platform | Audio | Notificaties |
48
+ |----------|-------|--------------|
49
+ | macOS | `afplay` (ingebouwd) | `osascript` (ingebouwd) |
50
+ | Linux | `mpv` of `ffplay` | `notify-send` |
51
+ | Windows | Windows Media Player | Windows Toast Notifications |
50
52
 
51
53
  ## Configuratie (optioneel)
52
54
 
@@ -56,12 +58,22 @@ Maak `~/.config/opencode/probleemwijken.json`:
56
58
  {
57
59
  "enabled": true,
58
60
  "includeBundledSounds": true,
59
- "customSoundsDir": "~/my-sounds",
61
+ "customSoundsDir": null,
62
+ "notifications": {
63
+ "enabled": true,
64
+ "timeout": 5
65
+ },
60
66
  "events": {
61
- "complete": true,
62
- "subagent_complete": false,
63
- "error": true,
64
- "permission": false
67
+ "complete": { "sound": true, "notification": true },
68
+ "subagent_complete": { "sound": false, "notification": false },
69
+ "error": { "sound": true, "notification": true },
70
+ "permission": { "sound": false, "notification": false }
71
+ },
72
+ "messages": {
73
+ "complete": "Sessie voltooid!",
74
+ "subagent_complete": "Subagent klaar",
75
+ "error": "Er is een fout opgetreden",
76
+ "permission": "Permissie nodig"
65
77
  }
66
78
  }
67
79
  ```
@@ -73,10 +85,43 @@ Maak `~/.config/opencode/probleemwijken.json`:
73
85
  | `enabled` | boolean | `true` | Plugin aan/uit |
74
86
  | `includeBundledSounds` | boolean | `true` | Probleemwijken geluiden gebruiken |
75
87
  | `customSoundsDir` | string | `null` | Pad naar folder met eigen geluiden |
76
- | `events.complete` | boolean | `true` | Geluid bij voltooide sessie |
77
- | `events.subagent_complete` | boolean | `false` | Geluid bij voltooide subagent |
78
- | `events.error` | boolean | `true` | Geluid bij error |
79
- | `events.permission` | boolean | `false` | Geluid bij permission request |
88
+ | `notifications.enabled` | boolean | `true` | Notificaties aan/uit |
89
+ | `notifications.timeout` | number | `5` | Notificatie timeout in seconden (Linux) |
90
+
91
+ ### Events
92
+
93
+ Per event kun je sound en notification apart aan/uit zetten:
94
+
95
+ ```json
96
+ {
97
+ "events": {
98
+ "complete": { "sound": true, "notification": true },
99
+ "error": { "sound": true, "notification": false }
100
+ }
101
+ }
102
+ ```
103
+
104
+ Of simpelweg een boolean voor beide:
105
+
106
+ ```json
107
+ {
108
+ "events": {
109
+ "complete": true,
110
+ "error": false
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### Berichten aanpassen
116
+
117
+ ```json
118
+ {
119
+ "messages": {
120
+ "complete": "Klaar!",
121
+ "error": "Oeps, er ging iets mis"
122
+ }
123
+ }
124
+ ```
80
125
 
81
126
  ### Eigen geluiden toevoegen
82
127
 
package/dist/index.js CHANGED
@@ -6,11 +6,21 @@ var DEFAULT_CONFIG = {
6
6
  enabled: true,
7
7
  customSoundsDir: null,
8
8
  includeBundledSounds: true,
9
+ notifications: {
10
+ enabled: true,
11
+ timeout: 5
12
+ },
9
13
  events: {
10
- complete: true,
11
- subagent_complete: false,
12
- error: true,
13
- permission: false
14
+ complete: { sound: true, notification: true },
15
+ subagent_complete: { sound: false, notification: false },
16
+ error: { sound: true, notification: true },
17
+ permission: { sound: false, notification: false }
18
+ },
19
+ messages: {
20
+ complete: "Sessie voltooid!",
21
+ subagent_complete: "Subagent klaar",
22
+ error: "Er is een fout opgetreden",
23
+ permission: "Permissie nodig"
14
24
  }
15
25
  };
16
26
  function loadConfig() {
@@ -31,25 +41,57 @@ function loadConfig() {
31
41
  try {
32
42
  const content = readFileSync(configPath, "utf-8");
33
43
  const userConfig = JSON.parse(content);
44
+ const parseEventConfig = (value, defaultValue) => {
45
+ if (typeof value === "boolean") {
46
+ return { sound: value, notification: value };
47
+ }
48
+ if (typeof value === "object" && value !== null) {
49
+ return {
50
+ sound: value.sound ?? defaultValue.sound,
51
+ notification: value.notification ?? defaultValue.notification
52
+ };
53
+ }
54
+ return defaultValue;
55
+ };
34
56
  return {
35
57
  enabled: userConfig.enabled ?? DEFAULT_CONFIG.enabled,
36
58
  customSoundsDir: userConfig.customSoundsDir ?? DEFAULT_CONFIG.customSoundsDir,
37
59
  includeBundledSounds: userConfig.includeBundledSounds ?? DEFAULT_CONFIG.includeBundledSounds,
60
+ notifications: {
61
+ enabled: userConfig.notifications?.enabled ?? DEFAULT_CONFIG.notifications.enabled,
62
+ timeout: userConfig.notifications?.timeout ?? DEFAULT_CONFIG.notifications.timeout
63
+ },
38
64
  events: {
39
- complete: userConfig.events?.complete ?? DEFAULT_CONFIG.events.complete,
40
- subagent_complete: userConfig.events?.subagent_complete ?? DEFAULT_CONFIG.events.subagent_complete,
41
- error: userConfig.events?.error ?? DEFAULT_CONFIG.events.error,
42
- permission: userConfig.events?.permission ?? DEFAULT_CONFIG.events.permission
65
+ complete: parseEventConfig(userConfig.events?.complete, DEFAULT_CONFIG.events.complete),
66
+ subagent_complete: parseEventConfig(userConfig.events?.subagent_complete, DEFAULT_CONFIG.events.subagent_complete),
67
+ error: parseEventConfig(userConfig.events?.error, DEFAULT_CONFIG.events.error),
68
+ permission: parseEventConfig(userConfig.events?.permission, DEFAULT_CONFIG.events.permission)
69
+ },
70
+ messages: {
71
+ complete: userConfig.messages?.complete ?? DEFAULT_CONFIG.messages.complete,
72
+ subagent_complete: userConfig.messages?.subagent_complete ?? DEFAULT_CONFIG.messages.subagent_complete,
73
+ error: userConfig.messages?.error ?? DEFAULT_CONFIG.messages.error,
74
+ permission: userConfig.messages?.permission ?? DEFAULT_CONFIG.messages.permission
43
75
  }
44
76
  };
45
77
  } catch {
46
78
  return DEFAULT_CONFIG;
47
79
  }
48
80
  }
49
- function isEventEnabled(config, event) {
81
+ function isSoundEnabled(config, event) {
82
+ if (!config.enabled)
83
+ return false;
84
+ return config.events[event]?.sound ?? false;
85
+ }
86
+ function isNotificationEnabled(config, event) {
50
87
  if (!config.enabled)
51
88
  return false;
52
- return config.events[event] ?? false;
89
+ if (!config.notifications.enabled)
90
+ return false;
91
+ return config.events[event]?.notification ?? false;
92
+ }
93
+ function getMessage(config, event) {
94
+ return config.messages[event] ?? "";
53
95
  }
54
96
 
55
97
  // src/sound.ts
@@ -192,6 +234,93 @@ async function playRandomSound(config) {
192
234
  } catch {}
193
235
  }
194
236
 
237
+ // src/notify.ts
238
+ import { platform as platform2 } from "os";
239
+ import { spawn as spawn2 } from "child_process";
240
+ async function runCommand2(command, args) {
241
+ return new Promise((resolve, reject) => {
242
+ const proc = spawn2(command, args, {
243
+ stdio: "ignore",
244
+ detached: false
245
+ });
246
+ proc.on("error", reject);
247
+ proc.on("close", (code) => {
248
+ if (code === 0)
249
+ resolve();
250
+ else
251
+ reject(new Error(`Exit code ${code}`));
252
+ });
253
+ });
254
+ }
255
+ async function notifyMac(title, message) {
256
+ const script = `display notification "${message}" with title "${title}"`;
257
+ await runCommand2("osascript", ["-e", script]);
258
+ }
259
+ async function notifyLinux(title, message, timeout) {
260
+ try {
261
+ await runCommand2("notify-send", ["-t", String(timeout * 1000), title, message]);
262
+ return;
263
+ } catch {
264
+ try {
265
+ await runCommand2("zenity", ["--notification", `--text=${title}: ${message}`]);
266
+ } catch {}
267
+ }
268
+ }
269
+ async function notifyWindows(title, message) {
270
+ const script = `
271
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
272
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
273
+ $template = @"
274
+ <toast>
275
+ <visual>
276
+ <binding template="ToastText02">
277
+ <text id="1">$($args[0])</text>
278
+ <text id="2">$($args[1])</text>
279
+ </binding>
280
+ </visual>
281
+ </toast>
282
+ "@
283
+ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
284
+ $xml.LoadXml($template)
285
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
286
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("OpenCode").Show($toast)
287
+ `;
288
+ try {
289
+ await runCommand2("powershell", ["-c", script, title, message]);
290
+ } catch {
291
+ const simpleScript = `
292
+ Add-Type -AssemblyName System.Windows.Forms
293
+ $balloon = New-Object System.Windows.Forms.NotifyIcon
294
+ $balloon.Icon = [System.Drawing.SystemIcons]::Information
295
+ $balloon.BalloonTipTitle = $args[0]
296
+ $balloon.BalloonTipText = $args[1]
297
+ $balloon.Visible = $true
298
+ $balloon.ShowBalloonTip(5000)
299
+ Start-Sleep -Seconds 1
300
+ $balloon.Dispose()
301
+ `;
302
+ try {
303
+ await runCommand2("powershell", ["-c", simpleScript, title, message]);
304
+ } catch {}
305
+ }
306
+ }
307
+ async function sendNotification(title, message, timeout = 5) {
308
+ const os = platform2();
309
+ try {
310
+ switch (os) {
311
+ case "darwin":
312
+ await notifyMac(title, message);
313
+ break;
314
+ case "linux":
315
+ await notifyLinux(title, message, timeout);
316
+ break;
317
+ case "win32":
318
+ await notifyWindows(title, message);
319
+ break;
320
+ }
321
+ } catch {}
322
+ }
323
+
195
324
  // src/index.ts
196
325
  function getSessionIDFromEvent(event) {
197
326
  const sessionID = event?.properties?.sessionID;
@@ -209,33 +338,38 @@ async function isChildSession(client, sessionID) {
209
338
  return false;
210
339
  }
211
340
  }
212
- var RandomSoundboardPlugin = async ({ client }) => {
341
+ async function handleEvent(config, eventType, projectName) {
342
+ const promises = [];
343
+ if (isSoundEnabled(config, eventType)) {
344
+ promises.push(playRandomSound(config));
345
+ }
346
+ if (isNotificationEnabled(config, eventType)) {
347
+ const title = projectName ? `OpenCode (${projectName})` : "OpenCode";
348
+ const message = getMessage(config, eventType);
349
+ promises.push(sendNotification(title, message, config.notifications.timeout));
350
+ }
351
+ await Promise.allSettled(promises);
352
+ }
353
+ var RandomSoundboardPlugin = async ({ client, directory }) => {
213
354
  const config = loadConfig();
355
+ const projectName = directory ? directory.split("/").pop() ?? null : null;
214
356
  return {
215
357
  event: async ({ event }) => {
216
358
  if (event.type === "permission.updated" || event.type === "permission.asked") {
217
- if (isEventEnabled(config, "permission")) {
218
- await playRandomSound(config);
219
- }
359
+ await handleEvent(config, "permission", projectName);
220
360
  }
221
361
  if (event.type === "session.idle") {
222
362
  const sessionID = getSessionIDFromEvent(event);
223
363
  if (sessionID) {
224
364
  const isChild = await isChildSession(client, sessionID);
225
365
  const eventType = isChild ? "subagent_complete" : "complete";
226
- if (isEventEnabled(config, eventType)) {
227
- await playRandomSound(config);
228
- }
366
+ await handleEvent(config, eventType, projectName);
229
367
  } else {
230
- if (isEventEnabled(config, "complete")) {
231
- await playRandomSound(config);
232
- }
368
+ await handleEvent(config, "complete", projectName);
233
369
  }
234
370
  }
235
371
  if (event.type === "session.error") {
236
- if (isEventEnabled(config, "error")) {
237
- await playRandomSound(config);
238
- }
372
+ await handleEvent(config, "error", projectName);
239
373
  }
240
374
  }
241
375
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-probleemwijken",
3
- "version": "1.1.0",
4
- "description": "OpenCode plugin that plays a random Probleemwijken/Derkolk soundboard sound when a session completes",
3
+ "version": "1.2.0",
4
+ "description": "OpenCode plugin that plays random Probleemwijken/Derkolk sounds and sends push notifications when a session completes",
5
5
  "author": "Daan-Friday",
6
6
  "license": "MIT",
7
7
  "type": "module",