opencode-probleemwijken 1.0.1 → 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 +92 -11
  2. package/dist/index.js +189 -37
  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,28 +44,107 @@ 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
 
53
- Maak `~/.config/opencode/probleemwijken.json` om events aan/uit te zetten:
55
+ Maak `~/.config/opencode/probleemwijken.json`:
54
56
 
55
57
  ```json
56
58
  {
57
59
  "enabled": true,
60
+ "includeBundledSounds": true,
61
+ "customSoundsDir": null,
62
+ "notifications": {
63
+ "enabled": true,
64
+ "timeout": 5
65
+ },
66
+ "events": {
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"
77
+ }
78
+ }
79
+ ```
80
+
81
+ ### Opties
82
+
83
+ | Optie | Type | Default | Beschrijving |
84
+ |-------|------|---------|--------------|
85
+ | `enabled` | boolean | `true` | Plugin aan/uit |
86
+ | `includeBundledSounds` | boolean | `true` | Probleemwijken geluiden gebruiken |
87
+ | `customSoundsDir` | string | `null` | Pad naar folder met eigen geluiden |
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
+ {
58
108
  "events": {
59
109
  "complete": true,
60
- "subagent_complete": false,
61
- "error": true,
62
- "permission": false
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"
63
122
  }
64
123
  }
65
124
  ```
66
125
 
126
+ ### Eigen geluiden toevoegen
127
+
128
+ 1. Maak een folder met je eigen MP3/WAV/OGG bestanden
129
+ 2. Configureer het pad in `probleemwijken.json`:
130
+
131
+ ```json
132
+ {
133
+ "customSoundsDir": "/home/user/my-sounds"
134
+ }
135
+ ```
136
+
137
+ De plugin kiest dan random uit zowel de Probleemwijken geluiden als je eigen geluiden.
138
+
139
+ ### Alleen eigen geluiden gebruiken
140
+
141
+ ```json
142
+ {
143
+ "includeBundledSounds": false,
144
+ "customSoundsDir": "/home/user/my-sounds"
145
+ }
146
+ ```
147
+
67
148
  ## Credits
68
149
 
69
150
  - Geluiden van [derkolk.nl/probleemwijken](https://www.derkolk.nl/probleemwijken/)
package/dist/index.js CHANGED
@@ -4,40 +4,94 @@ import { join } from "path";
4
4
  import { homedir } from "os";
5
5
  var DEFAULT_CONFIG = {
6
6
  enabled: true,
7
- soundsDir: null,
7
+ customSoundsDir: null,
8
+ includeBundledSounds: true,
9
+ notifications: {
10
+ enabled: true,
11
+ timeout: 5
12
+ },
8
13
  events: {
9
- complete: true,
10
- subagent_complete: false,
11
- error: true,
12
- 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"
13
24
  }
14
25
  };
15
26
  function loadConfig() {
16
- const configPath = join(homedir(), ".config", "opencode", "random-soundboard.json");
17
- if (!existsSync(configPath)) {
27
+ const configPaths = [
28
+ join(homedir(), ".config", "opencode", "probleemwijken.json"),
29
+ join(homedir(), ".config", "opencode", "random-soundboard.json")
30
+ ];
31
+ let configPath = null;
32
+ for (const path of configPaths) {
33
+ if (existsSync(path)) {
34
+ configPath = path;
35
+ break;
36
+ }
37
+ }
38
+ if (!configPath) {
18
39
  return DEFAULT_CONFIG;
19
40
  }
20
41
  try {
21
42
  const content = readFileSync(configPath, "utf-8");
22
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
+ };
23
56
  return {
24
57
  enabled: userConfig.enabled ?? DEFAULT_CONFIG.enabled,
25
- soundsDir: userConfig.soundsDir ?? DEFAULT_CONFIG.soundsDir,
58
+ customSoundsDir: userConfig.customSoundsDir ?? DEFAULT_CONFIG.customSoundsDir,
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
+ },
26
64
  events: {
27
- complete: userConfig.events?.complete ?? DEFAULT_CONFIG.events.complete,
28
- subagent_complete: userConfig.events?.subagent_complete ?? DEFAULT_CONFIG.events.subagent_complete,
29
- error: userConfig.events?.error ?? DEFAULT_CONFIG.events.error,
30
- 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
31
75
  }
32
76
  };
33
77
  } catch {
34
78
  return DEFAULT_CONFIG;
35
79
  }
36
80
  }
37
- function isEventEnabled(config, event) {
81
+ function isSoundEnabled(config, event) {
38
82
  if (!config.enabled)
39
83
  return false;
40
- return config.events[event] ?? false;
84
+ return config.events[event]?.sound ?? false;
85
+ }
86
+ function isNotificationEnabled(config, event) {
87
+ if (!config.enabled)
88
+ return 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] ?? "";
41
95
  }
42
96
 
43
97
  // src/sound.ts
@@ -58,15 +112,9 @@ function getBundledSoundsDir() {
58
112
  return path;
59
113
  }
60
114
  }
61
- return join2(__dirname2, "..", "sounds");
62
- }
63
- function getSoundsDirectory(config) {
64
- if (config.soundsDir && existsSync2(config.soundsDir)) {
65
- return config.soundsDir;
66
- }
67
- return getBundledSoundsDir();
115
+ return null;
68
116
  }
69
- function getSoundFiles(directory) {
117
+ function getSoundFilesFromDir(directory) {
70
118
  if (!existsSync2(directory)) {
71
119
  return [];
72
120
  }
@@ -80,6 +128,19 @@ function getSoundFiles(directory) {
80
128
  return [];
81
129
  }
82
130
  }
131
+ function getAllSoundFiles(config) {
132
+ const allSounds = [];
133
+ if (config.includeBundledSounds) {
134
+ const bundledDir = getBundledSoundsDir();
135
+ if (bundledDir) {
136
+ allSounds.push(...getSoundFilesFromDir(bundledDir));
137
+ }
138
+ }
139
+ if (config.customSoundsDir && existsSync2(config.customSoundsDir)) {
140
+ allSounds.push(...getSoundFilesFromDir(config.customSoundsDir));
141
+ }
142
+ return allSounds;
143
+ }
83
144
  function getRandomSound(sounds) {
84
145
  if (sounds.length === 0)
85
146
  return null;
@@ -147,8 +208,7 @@ async function playOnWindows(soundPath) {
147
208
  }
148
209
  }
149
210
  async function playRandomSound(config) {
150
- const soundsDir = getSoundsDirectory(config);
151
- const sounds = getSoundFiles(soundsDir);
211
+ const sounds = getAllSoundFiles(config);
152
212
  if (sounds.length === 0) {
153
213
  return;
154
214
  }
@@ -174,6 +234,93 @@ async function playRandomSound(config) {
174
234
  } catch {}
175
235
  }
176
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
+
177
324
  // src/index.ts
178
325
  function getSessionIDFromEvent(event) {
179
326
  const sessionID = event?.properties?.sessionID;
@@ -191,33 +338,38 @@ async function isChildSession(client, sessionID) {
191
338
  return false;
192
339
  }
193
340
  }
194
- 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 }) => {
195
354
  const config = loadConfig();
355
+ const projectName = directory ? directory.split("/").pop() ?? null : null;
196
356
  return {
197
357
  event: async ({ event }) => {
198
358
  if (event.type === "permission.updated" || event.type === "permission.asked") {
199
- if (isEventEnabled(config, "permission")) {
200
- await playRandomSound(config);
201
- }
359
+ await handleEvent(config, "permission", projectName);
202
360
  }
203
361
  if (event.type === "session.idle") {
204
362
  const sessionID = getSessionIDFromEvent(event);
205
363
  if (sessionID) {
206
364
  const isChild = await isChildSession(client, sessionID);
207
365
  const eventType = isChild ? "subagent_complete" : "complete";
208
- if (isEventEnabled(config, eventType)) {
209
- await playRandomSound(config);
210
- }
366
+ await handleEvent(config, eventType, projectName);
211
367
  } else {
212
- if (isEventEnabled(config, "complete")) {
213
- await playRandomSound(config);
214
- }
368
+ await handleEvent(config, "complete", projectName);
215
369
  }
216
370
  }
217
371
  if (event.type === "session.error") {
218
- if (isEventEnabled(config, "error")) {
219
- await playRandomSound(config);
220
- }
372
+ await handleEvent(config, "error", projectName);
221
373
  }
222
374
  }
223
375
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-probleemwijken",
3
- "version": "1.0.1",
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",