oc-sounds 0.1.0 → 0.1.2

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 CHANGED
@@ -15,9 +15,10 @@ Once the first release is published to npm, install globally with:
15
15
  opencode2 plugin add oc-sounds
16
16
  ```
17
17
 
18
- All sounds are enabled at **100% volume** by default. No individual sound setup
19
- is required. The package includes all 17 sound files and needs no browser or
20
- sound renderer at runtime.
18
+ All sounds are enabled at **85% volume** by default. No individual sound setup
19
+ is required. You can [change the sound for any action](#customize) in your OpenCode
20
+ config. The package includes all 17 sound files and needs no browser or sound
21
+ renderer at runtime.
21
22
 
22
23
  For project-only installation, add `oc-sounds` to `plugins` in your project's
23
24
  `opencode.jsonc` instead:
@@ -28,7 +29,7 @@ For project-only installation, add `oc-sounds` to `plugins` in your project's
28
29
  "plugins": [
29
30
  {
30
31
  "package": "oc-sounds",
31
- "options": { "volume": 1 }
32
+ "options": { "volume": 0.85 }
32
33
  }
33
34
  ]
34
35
  }
@@ -67,10 +68,22 @@ Sound plays on the **machine running the OpenCode server**. For a local TUI,
67
68
  desktop app, or browser connected to a local server, that is your computer. A
68
69
  remote/headless server needs its own accessible audio output.
69
70
 
70
- Playback runs asynchronously. Concurrent actions can overlap, and no action is
71
- throttled. If a player fails, automatic mode tries the next player; if none work,
72
- the plugin logs one warning and lets the agent continue. Unloading the plugin
73
- stops its audio processes and event subscription.
71
+ Playback runs asynchronously with burst smoothing enabled by default:
72
+
73
+ - Sound starts are spaced at least **180ms** apart per plugin instance.
74
+ - Repeated copies of the same sound within **300ms** are coalesced, including
75
+ duplicate cues waiting to play. Five simultaneous commands produce one start
76
+ cue and one return cue instead of ten overlapping sounds.
77
+ - At most **three cues** wait to play. Older routine cues are dropped when the
78
+ queue fills, and routine cues older than a second expire.
79
+ - Completion, errors, permission requests, questions, and interruptions take
80
+ priority over queued routine activity. Custom sound mappings retain this
81
+ priority.
82
+
83
+ The quiet tails of longer sounds can still overlap. If a player fails, automatic
84
+ mode tries the next player; if none work, the plugin logs one warning and lets
85
+ the agent continue. Unloading stops its queued cues, audio processes, and event
86
+ subscription.
74
87
 
75
88
  ## What makes a sound?
76
89
 
@@ -81,25 +94,28 @@ hooks. The built-in tool palette is:
81
94
  | Tool action | Sound |
82
95
  | --- | --- |
83
96
  | Read files | `page` |
84
- | Glob, grep, web search | `scan` |
85
- | Fetch a URL | `droplet` |
86
- | Write, edit, patch | `toggle` |
87
- | Shell / code execution | `pulse` |
97
+ | Glob and grep | `release` |
98
+ | Web search | `arrival` |
99
+ | Web fetch | `loading` |
100
+ | Write and edit | `toggle` |
101
+ | Patch a file | `arrival` |
102
+ | Shell / code execution | `tick` |
88
103
  | Delegate to a subagent | `arrival` |
89
104
  | Load a skill | `sparkle` |
90
105
  | Ask the user a question | `chime` |
91
106
  | Other tools | `tick` |
92
107
  | Tool succeeds / fails | `release` / `error` |
108
+ | Web fetch succeeds | `ready` |
93
109
 
94
110
  Session actions also have cues. The option keys are listed below:
95
111
 
96
112
  | Option key | Action | Default |
97
113
  | --- | --- | --- |
98
- | `prompt` | Input enters the session inbox | `press` |
114
+ | `prompt` | Input enters the session inbox | muted |
99
115
  | `start` | Agent execution starts | `loading` |
100
- | `step` | Model step starts | `pulse` |
116
+ | `step` | Model step starts | muted |
101
117
  | `thinking` | Reasoning block starts | `whisper` |
102
- | `reply` | Reply block starts | `chime` |
118
+ | `reply` | Model text / progress message starts | `scan` |
103
119
  | `tool` | Fallback tool-start cue | `tick` |
104
120
  | `toolComplete` | Tool completes | `release` |
105
121
  | `toolError` | Tool fails | `error` |
@@ -119,10 +135,27 @@ Reasoning and replies sound once per block, rather than on every streamed token.
119
135
  Tool progress updates do not repeat the start cue. Public events are scoped to
120
136
  the plugin's location and repeated event IDs are deduplicated.
121
137
 
138
+ `reply` covers model text, including mid-task updates such as "I'll check the
139
+ files now." OpenCode's text-start event does not distinguish intermediate text
140
+ from the final reply, so both use `scan`. The separate `complete` cue plays
141
+ `success` when the agent finishes its execution.
142
+
143
+ OpenCode emits `prompt`, `start`, and `step` almost simultaneously for ordinary
144
+ prompts, including before any text is visible. Only `start` is audible by
145
+ default, producing one `loading` cue rather than a three-sound burst. Set
146
+ `prompt` to `"press"` or `step` to `"pulse"` in `cues` if you want those extra
147
+ lifecycle cues.
148
+
122
149
  ## Customize
123
150
 
151
+ **Every action's sound is configurable.** Use `cues` for session actions such as
152
+ thinking, replying, and completion, `tools` for individual tool-start sounds,
153
+ and `returns` for successful per-tool return sounds. Set a value to any Cuelume
154
+ sound name, or `false` to mute it. Omitted settings keep their defaults.
155
+
124
156
  Edit the existing plugin entry in `~/.config/opencode/opencode.jsonc` (global)
125
- or your project's `opencode.jsonc`. Merge it with your other settings:
157
+ or your project's `opencode.jsonc`. Merge it with your other settings. If you use
158
+ a local checkout, keep its directory path in `package`:
126
159
 
127
160
  ```jsonc
128
161
  {
@@ -132,17 +165,29 @@ or your project's `opencode.jsonc`. Merge it with your other settings:
132
165
  "package": "oc-sounds",
133
166
  "options": {
134
167
  "enabled": true,
135
- "volume": 0.35,
168
+ "volume": 0.85,
136
169
  "player": "auto",
170
+ "playback": {
171
+ "minIntervalMs": 180,
172
+ "dedupeWindowMs": 300
173
+ },
137
174
  "cues": {
175
+ "prompt": "press",
138
176
  "thinking": "whisper",
177
+ "reply": "scan",
139
178
  "complete": "ready",
140
179
  "step": false
141
180
  },
142
181
  "tools": {
143
182
  "read": "tick",
183
+ "edit": "toggle",
144
184
  "shell": "pulse",
185
+ "webfetch": "loading",
186
+ "websearch": "arrival",
145
187
  "my_mcp_tool": "sparkle"
188
+ },
189
+ "returns": {
190
+ "webfetch": "ready"
146
191
  }
147
192
  }
148
193
  }
@@ -150,10 +195,24 @@ or your project's `opencode.jsonc`. Merge it with your other settings:
150
195
  }
151
196
  ```
152
197
 
198
+ This example changes reads to `tick`, completion to `ready`, and mutes model-step
199
+ cues. Change any name to choose a different sound. For all available action and
200
+ tool mappings in one file, see [`examples/opencode.json`](examples/opencode.json),
201
+ which is also included in the npm package.
202
+
203
+ Save your OpenCode config to apply the changes. If the running plugin does not
204
+ reload automatically, run `opencode2 service restart`. Changing these options
205
+ does not require rebuilding or reinstalling the plugin.
206
+
153
207
  - `enabled`: defaults to `true`. Set to `false` to disable all sounds.
154
- - `volume`: from `0` to `1`, defaults to `1` (100%). Zero disables playback.
208
+ - `volume`: from `0` to `1`, defaults to `0.85` (85%). Zero disables playback.
155
209
  - `player`: `auto`, `paplay`, `pw-play`, `afplay`, or `ffplay`. An explicit player
156
210
  disables automatic fallback to other players.
211
+ - `playback.minIntervalMs`: minimum time between sound starts, defaults to `180`.
212
+ - `playback.dedupeWindowMs`: cooldown for repeating the same sound, defaults to
213
+ `300`. Both timing values accept integers from `0` to `5000` milliseconds. Set
214
+ both to `0` for immediate, unthrottled playback. For a calmer pace, try `250`
215
+ and `500` respectively.
157
216
  - `cues`: override any action in the table, or set it to `false` to mute it.
158
217
  Setting `cues.tool` replaces the built-in starting palette for all ordinary
159
218
  tools. `question` is configured separately.
@@ -161,6 +220,10 @@ or your project's `opencode.jsonc`. Merge it with your other settings:
161
220
  name (`read` also matches `functions.read` / `functions_read`). Exact names
162
221
  take precedence. `false` mutes that tool's starting cue; completion and error
163
222
  cues are controlled separately. Tool overrides take precedence over `cues.tool`.
223
+ - `returns`: override successful return cues by exact effective tool name or
224
+ built-in short name. The default `webfetch` return is `ready`; other successful
225
+ tools fall back to `cues.toolComplete`. Set a value to `false` to mute that
226
+ tool's successful return without muting its starting cue.
164
227
 
165
228
  All 17 Cuelume sounds are available:
166
229
  `chime`, `sparkle`, `droplet`, `bloom`, `whisper`, `tick`, `press`, `release`,
@@ -192,7 +255,7 @@ project OpenCode configuration:
192
255
  "plugins": [
193
256
  {
194
257
  "package": "/absolute/path/to/oc-sounds",
195
- "options": { "volume": 1 }
258
+ "options": { "volume": 0.85 }
196
259
  }
197
260
  ]
198
261
  }
@@ -217,8 +280,9 @@ device. Cuelume and the native offline renderer are development dependencies;
217
280
  neither is needed by the installed plugin.
218
281
 
219
282
  Tests cover the V2 plugin lifecycle, event isolation/deduplication, concurrent
220
- tools, muting, cleanup, player fallback, and bundled WAV integrity. Linux player
221
- tests use fake executables and do not play audio.
283
+ tools, burst coalescing, playback spacing and priority, bounded queues, muting,
284
+ cleanup, player fallback, and bundled WAV integrity. Scheduler tests use a fake
285
+ clock; Linux player tests use fake executables and do not play audio.
222
286
 
223
287
  ### Publish to npm
224
288
 
@@ -230,8 +294,8 @@ npm pack --dry-run
230
294
  ```
231
295
 
232
296
  The `prepack` hook builds the plugin automatically. The package contains `dist/`,
233
- `sounds/`, the README, and the license notices; development dependencies and
234
- rendering scripts are excluded.
297
+ `sounds/`, `examples/`, the README, and the license notices; development
298
+ dependencies and rendering scripts are excluded.
235
299
 
236
300
  Sign in to an npm account with publishing access, then publish:
237
301
 
package/dist/config.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  export declare const sounds: readonly ["chime", "sparkle", "droplet", "bloom", "whisper", "tick", "press", "release", "toggle", "success", "error", "page", "loading", "ready", "pulse", "scan", "arrival"];
2
2
  export type SoundName = (typeof sounds)[number];
3
3
  export declare const defaultCues: {
4
- readonly prompt: "press";
4
+ readonly prompt: false;
5
5
  readonly start: "loading";
6
- readonly step: "pulse";
6
+ readonly step: false;
7
7
  readonly thinking: "whisper";
8
- readonly reply: "chime";
8
+ readonly reply: "scan";
9
9
  readonly tool: "tick";
10
10
  readonly toolComplete: "release";
11
11
  readonly toolError: "error";
@@ -23,12 +23,23 @@ export declare const defaultCues: {
23
23
  };
24
24
  export type Action = keyof typeof defaultCues;
25
25
  export declare const defaultToolCues: Record<string, SoundName>;
26
+ export declare const defaultToolReturnCues: Record<string, SoundName>;
26
27
  export type PlayerName = "auto" | "paplay" | "pw-play" | "afplay" | "ffplay";
28
+ export declare const defaultPlayback: {
29
+ minIntervalMs: number;
30
+ dedupeWindowMs: number;
31
+ };
32
+ export interface PlaybackOptions {
33
+ minIntervalMs: number;
34
+ dedupeWindowMs: number;
35
+ }
27
36
  export interface Options {
28
37
  enabled: boolean;
29
38
  volume: number;
30
39
  player: PlayerName;
40
+ playback: PlaybackOptions;
31
41
  cues: Record<Action, SoundName | false>;
32
42
  tools: Record<string, SoundName | false>;
43
+ returns: Record<string, SoundName | false>;
33
44
  }
34
45
  export declare function parseOptions(input: unknown): Options;
package/dist/config.js CHANGED
@@ -4,11 +4,11 @@ export const sounds = [
4
4
  "pulse", "scan", "arrival",
5
5
  ];
6
6
  export const defaultCues = {
7
- prompt: "press",
7
+ prompt: false,
8
8
  start: "loading",
9
- step: "pulse",
9
+ step: false,
10
10
  thinking: "whisper",
11
- reply: "chime",
11
+ reply: "scan",
12
12
  tool: "tick",
13
13
  toolComplete: "release",
14
14
  toolError: "error",
@@ -26,21 +26,25 @@ export const defaultCues = {
26
26
  };
27
27
  export const defaultToolCues = {
28
28
  read: "page",
29
- glob: "scan",
30
- grep: "scan",
31
- websearch: "scan",
32
- webfetch: "droplet",
29
+ glob: "release",
30
+ grep: "release",
31
+ websearch: "arrival",
32
+ webfetch: "loading",
33
33
  write: "toggle",
34
34
  edit: "toggle",
35
- patch: "toggle",
36
- apply_patch: "toggle",
37
- shell: "pulse",
38
- bash: "pulse",
39
- execute: "pulse",
35
+ patch: "arrival",
36
+ apply_patch: "arrival",
37
+ shell: "tick",
38
+ bash: "tick",
39
+ execute: "tick",
40
40
  subagent: "arrival",
41
41
  task: "arrival",
42
42
  skill: "sparkle",
43
43
  };
44
+ export const defaultToolReturnCues = {
45
+ webfetch: "ready",
46
+ };
47
+ export const defaultPlayback = { minIntervalMs: 180, dedupeWindowMs: 300 };
44
48
  function object(value) {
45
49
  if (value && typeof value === "object" && !Array.isArray(value)) {
46
50
  return value;
@@ -55,14 +59,14 @@ function cue(value) {
55
59
  export function parseOptions(input) {
56
60
  const raw = object(input);
57
61
  for (const key of Object.keys(raw)) {
58
- if (!["enabled", "volume", "player", "cues", "tools"].includes(key)) {
62
+ if (!["enabled", "volume", "player", "playback", "cues", "tools", "returns"].includes(key)) {
59
63
  throw new Error(`oc-sounds: unknown option ${key}`);
60
64
  }
61
65
  }
62
66
  const enabled = raw.enabled ?? true;
63
67
  if (typeof enabled !== "boolean")
64
68
  throw new Error("oc-sounds: enabled must be a boolean");
65
- const volume = raw.volume ?? 1;
69
+ const volume = raw.volume ?? 0.85;
66
70
  if (typeof volume !== "number" || !Number.isFinite(volume) || volume < 0 || volume > 1) {
67
71
  throw new Error("oc-sounds: volume must be between 0 and 1");
68
72
  }
@@ -79,5 +83,17 @@ export function parseOptions(input) {
79
83
  const tools = Object.create(null);
80
84
  for (const [key, value] of Object.entries(object(raw.tools ?? {})))
81
85
  tools[key] = cue(value);
82
- return { enabled, volume, player: player, cues, tools };
86
+ const returns = Object.create(null);
87
+ for (const [key, value] of Object.entries(object(raw.returns ?? {})))
88
+ returns[key] = cue(value);
89
+ const playback = { ...defaultPlayback };
90
+ for (const [key, value] of Object.entries(object(raw.playback ?? {}))) {
91
+ if (!Object.hasOwn(playback, key))
92
+ throw new Error(`oc-sounds: unknown playback option ${key}`);
93
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 5000) {
94
+ throw new Error(`oc-sounds: playback.${key} must be an integer between 0 and 5000 milliseconds`);
95
+ }
96
+ playback[key] = value;
97
+ }
98
+ return { enabled, volume, player: player, playback, cues, tools, returns };
83
99
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Plugin } from "@opencode/plugin";
2
2
  import { type Action, type Options } from "./config.js";
3
3
  import { type Player } from "./player.js";
4
- export { defaultCues, defaultToolCues, sounds } from "./config.js";
5
- export type { Action, Options, SoundName } from "./config.js";
4
+ export { defaultCues, defaultToolCues, defaultToolReturnCues, defaultPlayback, sounds } from "./config.js";
5
+ export type { Action, Options, PlaybackOptions, SoundName } from "./config.js";
6
6
  type Event = ReturnType<Plugin.Context["event"]["subscribe"]> extends AsyncIterable<infer E> ? E : never;
7
7
  export declare function actionForEvent(event: Event): Action | undefined;
8
8
  export declare function setup(ctx: Plugin.Context, makePlayer?: (selection: Options["player"], volume: number) => Player): Promise<Plugin.Cleanup | void>;
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { Plugin } from "@opencode/plugin";
2
2
  import { resolve } from "node:path";
3
3
  import { setTimeout as delay } from "node:timers/promises";
4
- import { defaultToolCues, parseOptions } from "./config.js";
4
+ import { defaultToolCues, defaultToolReturnCues, parseOptions } from "./config.js";
5
5
  import { createPlayer } from "./player.js";
6
- export { defaultCues, defaultToolCues, sounds } from "./config.js";
6
+ import { schedulePlayer } from "./scheduler.js";
7
+ export { defaultCues, defaultToolCues, defaultToolReturnCues, defaultPlayback, sounds } from "./config.js";
7
8
  const eventActions = {
8
9
  "session.inbox.enqueued": "prompt",
9
10
  "session.execution.started": "start",
@@ -25,33 +26,49 @@ const eventActions = {
25
26
  export function actionForEvent(event) {
26
27
  return eventActions[event.type];
27
28
  }
29
+ function shortToolName(tool) {
30
+ return tool.split(".").at(-1).replace(/^(functions|tools)_/, "");
31
+ }
32
+ const importantActions = new Set([
33
+ "complete", "error", "toolError", "permission", "question", "interrupt",
34
+ ]);
28
35
  // Exported separately to exercise the actual plugin lifecycle with a fake player.
29
36
  export async function setup(ctx, makePlayer = createPlayer) {
30
37
  const options = parseOptions(ctx.options);
31
38
  if (!options.enabled || options.volume === 0)
32
39
  return;
33
- const player = makePlayer(options.player, options.volume);
40
+ const player = schedulePlayer(makePlayer(options.player, options.volume), options.playback);
34
41
  const controller = new AbortController();
35
42
  const registrations = [];
43
+ const calls = new Map();
36
44
  const emit = (action, tool) => {
37
45
  if (controller.signal.aborted)
38
46
  return;
39
47
  let sound = options.cues[action];
40
48
  if (tool !== undefined) {
41
- const name = tool.split(".").at(-1);
42
- const short = name.replace(/^(functions|tools)_/, "");
43
- if (Object.hasOwn(options.tools, tool))
44
- sound = options.tools[tool];
45
- else if (Object.hasOwn(options.tools, short))
46
- sound = options.tools[short];
47
- // An explicit tool cue replaces the built-in palette for all tools.
48
- else if (action === "tool" && !Object.hasOwn(ctx.options.cues ?? {}, "tool")) {
49
- if (Object.hasOwn(defaultToolCues, short))
50
- sound = defaultToolCues[short];
49
+ const short = shortToolName(tool);
50
+ if (action === "tool") {
51
+ if (Object.hasOwn(options.tools, tool))
52
+ sound = options.tools[tool];
53
+ else if (Object.hasOwn(options.tools, short))
54
+ sound = options.tools[short];
55
+ // An explicit general cue replaces the built-in palette for all tools.
56
+ else if (!Object.hasOwn(ctx.options.cues ?? {}, "tool")) {
57
+ if (Object.hasOwn(defaultToolCues, short))
58
+ sound = defaultToolCues[short];
59
+ }
60
+ }
61
+ else if (action === "toolComplete") {
62
+ if (Object.hasOwn(options.returns, tool))
63
+ sound = options.returns[tool];
64
+ else if (Object.hasOwn(options.returns, short))
65
+ sound = options.returns[short];
66
+ else if (Object.hasOwn(defaultToolReturnCues, short))
67
+ sound = defaultToolReturnCues[short];
51
68
  }
52
69
  }
53
70
  if (sound)
54
- player.play(sound);
71
+ player.play(sound, importantActions.has(action));
55
72
  };
56
73
  const cleanup = async () => {
57
74
  controller.abort();
@@ -60,11 +77,14 @@ export async function setup(ctx, makePlayer = createPlayer) {
60
77
  };
61
78
  try {
62
79
  registrations.push(await ctx.tool.hook("execute.before", (event) => {
63
- const name = event.tool.split(/[._]/).at(-1);
80
+ const name = shortToolName(event.tool);
81
+ calls.set(event.id, event.tool);
64
82
  emit(name === "question" ? "question" : "tool", event.tool);
65
83
  }));
66
84
  registrations.push(await ctx.tool.hook("execute.after", (event) => {
67
- emit(event.status === "error" ? "toolError" : "toolComplete");
85
+ const tool = calls.get(event.id) ?? event.tool;
86
+ calls.delete(event.id);
87
+ emit(event.status === "error" ? "toolError" : "toolComplete", typeof tool === "string" ? tool : undefined);
68
88
  }));
69
89
  }
70
90
  catch (error) {
package/dist/player.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { PlayerName, SoundName } from "./config.js";
2
2
  type Backend = Exclude<PlayerName, "auto">;
3
3
  export interface Player {
4
- play(sound: SoundName): void;
4
+ play(sound: SoundName, important?: boolean): void;
5
5
  dispose(): void;
6
6
  }
7
7
  export declare function playerArgs(player: Backend, file: string, volume: number): string[];
@@ -0,0 +1,4 @@
1
+ import type { PlaybackOptions } from "./config.js";
2
+ import type { Player } from "./player.js";
3
+ /** One queue per plugin instance, shared by hooks and the session event stream. */
4
+ export declare function schedulePlayer(player: Player, options: PlaybackOptions, now?: () => number): Player;
@@ -0,0 +1,66 @@
1
+ const MAX_PENDING = 3;
2
+ const MAX_AGE_MS = 1000;
3
+ /** One queue per plugin instance, shared by hooks and the session event stream. */
4
+ export function schedulePlayer(player, options, now = () => performance.now()) {
5
+ let queue = [];
6
+ const recent = new Map();
7
+ let lastStart = -Infinity;
8
+ let timer;
9
+ let disposed = false;
10
+ function pump() {
11
+ if (disposed || timer)
12
+ return;
13
+ const time = now();
14
+ queue = queue.filter((item) => item.important || time - item.time <= MAX_AGE_MS);
15
+ if (!queue.length)
16
+ return;
17
+ const wait = options.minIntervalMs - (time - lastStart);
18
+ if (wait > 0) {
19
+ timer = setTimeout(() => {
20
+ timer = undefined;
21
+ pump();
22
+ }, Math.ceil(wait));
23
+ timer.unref();
24
+ return;
25
+ }
26
+ const item = queue.shift();
27
+ lastStart = time;
28
+ recent.set(item.sound, { time, important: item.important });
29
+ player.play(item.sound, item.important);
30
+ pump();
31
+ }
32
+ return {
33
+ play(sound, important = false) {
34
+ if (disposed)
35
+ return;
36
+ const time = now();
37
+ const previous = recent.get(sound);
38
+ if (previous && time - previous.time < options.dedupeWindowMs
39
+ && (!important || previous.important))
40
+ return;
41
+ // Attention/finish cues replace stale routine work instead of waiting
42
+ // behind a flood of tools. Priority follows the action, not its sound.
43
+ if (important)
44
+ queue = queue.filter((item) => item.important);
45
+ else if (queue.some((item) => item.important))
46
+ return;
47
+ if (queue.some((item) => item.sound === sound))
48
+ return;
49
+ if (queue.length >= MAX_PENDING)
50
+ queue.shift();
51
+ queue.push({ sound, important, time });
52
+ pump();
53
+ },
54
+ dispose() {
55
+ if (disposed)
56
+ return;
57
+ disposed = true;
58
+ if (timer)
59
+ clearTimeout(timer);
60
+ timer = undefined;
61
+ queue = [];
62
+ recent.clear();
63
+ player.dispose();
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "plugins": [
4
+ {
5
+ "package": "oc-sounds",
6
+ "options": {
7
+ "enabled": true,
8
+ "volume": 0.85,
9
+ "player": "auto",
10
+ "playback": {
11
+ "minIntervalMs": 180,
12
+ "dedupeWindowMs": 300
13
+ },
14
+ "cues": {
15
+ "prompt": false,
16
+ "start": "loading",
17
+ "step": false,
18
+ "thinking": "whisper",
19
+ "reply": "scan",
20
+ "tool": "tick",
21
+ "toolComplete": "release",
22
+ "toolError": "error",
23
+ "permission": "bloom",
24
+ "permissionReply": "toggle",
25
+ "question": "chime",
26
+ "retry": "scan",
27
+ "complete": "success",
28
+ "error": "error",
29
+ "interrupt": "droplet",
30
+ "compact": "page",
31
+ "compactComplete": "ready",
32
+ "skill": "sparkle",
33
+ "agent": "arrival"
34
+ },
35
+ "tools": {
36
+ "read": "page",
37
+ "glob": "release",
38
+ "grep": "release",
39
+ "websearch": "arrival",
40
+ "webfetch": "loading",
41
+ "write": "toggle",
42
+ "edit": "toggle",
43
+ "patch": "arrival",
44
+ "apply_patch": "arrival",
45
+ "shell": "tick",
46
+ "bash": "tick",
47
+ "execute": "tick",
48
+ "subagent": "arrival",
49
+ "task": "arrival",
50
+ "skill": "sparkle"
51
+ },
52
+ "returns": {
53
+ "webfetch": "ready"
54
+ }
55
+ }
56
+ }
57
+ ]
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-sounds",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Cuelume sound feedback for OpenCode V2 agent actions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  "exports": {
10
10
  ".": "./dist/index.js"
11
11
  },
12
- "files": ["dist", "sounds", "THIRD_PARTY_NOTICES.md"],
12
+ "files": ["dist", "sounds", "examples", "THIRD_PARTY_NOTICES.md"],
13
13
  "engines": { "node": ">=22" },
14
14
  "scripts": {
15
15
  "build": "tsc",