SLOPSHOPPER

findmutlu-sounds

Sound packs for the moments that need you: a prompt sent, a permission prompt, a long job done, a subagent back, a failed test, a git push, a compaction. Packs…

newguardcommandtoastpromptprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · findmutlu-sounds
› fix the failing auth test and add an audit log call ╭────────────────────────────────────────────╮ │ findmutlu-sounds │ ⏺ Read(src/auth.ts) │ Sounds: terran pack. Type /sounds for the │ ⎿ Read 6 lines │ others │ ⏺ Update(src/auth.ts) ╰────────────────────────────────────────────╯ ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /sounds ⎿ findmutlu-sounds: This terminal: terran ⎿ findmutlu-sounds: This project: none (/sounds project <pack> to give it one) ⎿ findmutlu-sounds: New terminals: terran ⎿ findmutlu-sounds: Sound: on, always, quiet 23:00 to 07:00 off, a prompt sound every prompt ⎿ findmutlu-sounds: Moments (what plays when): ⎿ findmutlu-sounds: ordered: you send a prompt (every prompt) → terran (2 clips, random) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

findmutlu-sounds

Funny sounds for Claude Code agent notifications. Pick a pack, or bring your own.

A Claude Code mod that plays a short sound at the moments that need you, and at no other time: Claude asks for your OK, a long job finishes, a test fails, a push goes through. Pick a Terran SCV, a Protoss zealot, an Age of Empires monk or Turkish villager, a Kirov airship, a Diablo II necromancer, Duke Nukem, Pickle Rick, Caleb from Blood, or the NASA radio. Part of claude-plugins.

The teaser: a Claude Code session runs npm test, one test fails, and StarCraft's "Nuclear launch detected" plays. Turn the sound on.

Watch the teaser with sound (28 seconds).

How it works

The how it works film: it opens on the pack logos, then a Claude session, drawn as a Fat Invader at its laptop, sends each moment to a speaker, and the speaker plays that moment's sound. Turn the sound on.

Watch it with sound (33 seconds, a famous line from a different pack at each moment: peon, terran, protoss, red-alert, rick-and-morty, duke, aoe).

MomentWhen
orderedYou send a prompt (an order given); every prompt by default, /sounds rest adds a wait
needsYouClaude waits for you: a permission prompt, a question for you or a plan to approve
longDoneA turn that took 60 seconds or more is answered
subagentStartA subagent starts (a villager is created, from the aoe pack, whatever your pack)
subagentA subagent finishes
failedA test, build, lint or type check fails (a step such as npm test, pytest, tsc or scripts/check-x.sh exits with an error; text inside a heredoc never counts)
pushedgit push goes through
compactedThe conversation is compacted
mcpAn MCP tool runs, at most every 5 minutes. Off until you turn it on (/sounds mcp aoe fires a trebuchet)

At most one sound every 2.5 seconds across all your terminals, so a handful of subagents finishing together give one sound, not a choir. The same clip never plays twice in a row for a moment. A pack may leave a moment silent.

The packs

The pack tour: every pack on one grid, and the best known ones lift their card in turn and play their famous line. Turn the sound on.

Hear the packs (41 seconds).

PackWhat it sounds likeSource
aoe"Wololo", the horn, the villager's "Yes", and the taunts: "Start the game already!", "What age are you in?", "You played two hours to die like this?" at a failed test, "Raiding party!" on a pushAge of Empires II, under Microsoft's Game Content Usage Rules
aoe-turkThe Turks: the villager (male and female), "Allah Allah!" and swords on a push, the victory fanfare after a long job; "Emrin?" (your order?), "Tamam" (OK), "Hazir" (ready), "Saldir!" (attack!) on a push, the job names when a subagent returnsAge of Empires II, under Microsoft's Game Content Usage Rules, via the openpeon pack by Yusuf Kinatas
terran (default)"SCV good to go, sir", "You want a piece of me, boy?", "Nuclear launch detected", "In the pipe, five by five"StarCraft, Blizzard Entertainment
protoss"En Taro Adun", "You must construct additional pylons", "My life for Aiur!", "Power overwhelming"StarCraft, Blizzard Entertainment
red-alert"Kirov reporting", "For mother Russia", "Your mind is clear", "Kaboom"Command & Conquer: Red Alert 2, Electronic Arts
peon"Work work", "Zug zug", "Something need doing?", "Me not that kind of orc"Warcraft III: Reign of Chaos, Blizzard Entertainment
necromancer"Raise skeleton" when a subagent returns, "Not enough mana", "I can't carry any more" at compactionDiablo II: Lord of Destruction, Blizzard Entertainment
diabloThe level up chime, the quest done fanfare, gold dropping, a town portal opening, an item breakingDiablo II: Lord of Destruction, Blizzard Entertainment
rick-and-morty"Wubba lubba dub dub", "Show me what you got!", "I'm Pickle Rick!" on a push, the butter robot's "What is my purpose?" when a subagent returns, "Oh jeeze" at a failed testRick and Morty, Warner Bros. Discovery (Adult Swim), via the family-friendly openpeon pack by Mr3zee
rick-and-morty-uncutThe swearing lines: "You son of a bitch, I'm in!", "Oh fuck me!", "Everybody fuck off!" on a push. Explicit language, never the defaultRick and Morty, Warner Bros. Discovery (Adult Swim), via the mature openpeon pack by Mr3zee
meeseeksMr. Meeseeks: "Can do!", "I'm Mr. Meeseeks, look at me!" when a subagent returns, "All done!", "I can't take it anymore!" at a failed testRick and Morty, Warner Bros. Discovery (Adult Swim), via the openpeon pack by kasperhendriks
duke"Come get some", "Hail to the king, baby", "Groovy", "Ooh, that's gotta hurt"Duke Nukem 3D, Gearbox Software
bloodCaleb: "Let's go", "I won", "Boomstick", "Pathetic", the laughBlood (Monolith Productions, 1997)
lichThe Lich and Kel'Thuzad: "As the master commands", "Thy bidding, master?", "Kneel before my Frost Nova!" on a push, "The Scourge will consume all!" at compactionWarcraft III, Blizzard Entertainment, via the openpeon pack
cabalCABAL, the Nod AI: "Establishing battlefield control", "Primary objective achieved", "Reinforcements have arrived" when a subagent returns, "Construct more power plants" at compactionCommand & Conquer: Tiberian Sun, Electronic Arts, via the openpeon pack
yuri"Your thoughts are mine", "I foresaw this need", "Another pawn joins us" when a subagent returns, "I did not foresee this" at a failed testRed Alert 2: Yuri's Revenge, Electronic Arts, via the openpeon pack
abathurThe Zerg evolution master, in his own long winded way: evolution done for a long job, a help call when Claude waitsStarCraft II, Blizzard Entertainment, via the openpeon pack
tauren"Peace and patience, friend", "Fate smiles upon you", "For the Horde!" on a push, "My inventory is full" at compactionWorld of Warcraft, Blizzard Entertainment, via the openpeon pack
engineer"Teleporter comin' right up", "Done and done", "You just ain't doin' it right" at a failed test, "Yeehaw!" on a pushTeam Fortress 2, Valve, via the openpeon pack
counterstrikeThe radio calls: "Roger that", "Need backup!", "Area clear!", "Enemy spotted!", "Lock and load!" on a pushCounter-Strike, Valve, via the openpeon pack
strongholdThe AI lords, a different one each time: Richard's "The lion roars", the Rat's "Plans worked perfectly", the Abbot's "My troops fail me", the Sheriff's "Not Father Christmas"Stronghold Crusader, FireFly Studios, via the openpeon packs of the lords (for example the Abbot)
sheogorathThe Prince of Madness: "Good choice", "I've been waiting for you", "Cheese for everyone", "Boring, boring, boring" at a failed testThe Elder Scrolls IV: Oblivion, Bethesda (ZeniMax), via the openpeon pack
lebowskiThe clean lines: "The Dude abides", "Phone's ringing, Dude", "That rug really tied the room together", "Over the line!", "Mark it zero!"The Big Lebowski (1998), Universal Pictures, via the openpeon pack
nasa"The Eagle has landed", "And the liftoff!", "Okay Houston, we have had a problem here", a Quindar beepNASA recordings on Wikimedia Commons, public domain

Each pack's folder under sounds/ has a CREDITS.md with the source of every clip and who holds its rights. The game clips are the original files as shared on 101soundboards and in openpeon packs, unchanged except that WAV and Ogg originals are stored as M4A (smaller, and macOS afplay stops Ogg clips after about a second), and the short intro 101soundboards puts before some clips is cut where the site's own player skips it. They belong to their publishers: this plugin is free fan use, not licensed, not endorsed by any of them, and never sold. If you hold the rights to a pack and want it gone, open an issue and it goes. The game and show logos in the banner and films are trademarks of their owners, shown only to name the pack's source, on the same terms.

Use

CommandEffect
/soundsThis terminal's pack, this project's pack, the default for new terminals and the settings, then what every moment plays, every command, and every pack at the bottom
/sounds <pack>Switch this terminal only
/sounds default <pack>Every terminal plays this pack: the open ones (in both accounts) from their next sound, and every new one. Project packs stay, and a terminal's own /sounds <pack> afterwards still wins there
/sounds project <pack>Every terminal in this folder, or a folder inside it, plays this pack, now and in every later session, in both accounts. One pack per project tells you by ear which one needs you
/sounds project off, /sounds projectTake this folder's pack back, or see this project's pack and every project pack
/sounds project clearRemove every project pack at once; the answer lists what it removed
/sounds <moment> <pack>One moment plays another pack's clips, e.g. /sounds pushed aoe-turk: each push plays one of that pack's push clips
/sounds <moment> <pack> <clip>Always the same clip, e.g. /sounds pushed aoe-turk allah-allah
/sounds <moment> <pack> <pack>Mix two or more packs' clips for that moment
/sounds <moment> randomEach time a clip from any pack
/sounds <moment> off, /sounds <moment> resetSilence one moment, or give it back to the terminal's pack
/sounds <moment>What that moment plays now and the names of its clips
/sounds test [pack] [moment]Play every sound of a pack once, or only one moment of it (/sounds test aoe-turk pushed), whatever the settings
/sounds off, /sounds onMute, unmute
/sounds away, /sounds alwaysPlay only while no terminal or editor is the front app, or always (the default). The prompt answer still plays in away mode: you are at the keyboard
/sounds night off, /sounds night onQuiet hours: no clips from 23:00 to 07:00. Off by default
/sounds rest <minutes>How long after this terminal's last prompt sound the next prompt plays one; 0 (the default) plays every prompt

Every change answers with one line saying what changed, and a mistyped pack or clip name gets the one it meant ("Did you mean aoe-turk?"). Moment names are typed in lowercase (/sounds needsyou); /sounds lists them, and a short word like push, fail or done gets the moment it means.

Install asks nothing. The first session shows one notice naming the pack and /sounds. Settings, moment rules included, are kept in the plugin's store, per Claude Code config directory.

Your own packs

A folder under ~/.config/findmutlu-sounds/packs/<name>/ (lowercase letters, digits and dashes) with your clips and a pack.json is a pack. It loads when a session starts and then works like the others (/sounds <name>, /sounds default <name>):

{
  "label": "My office",
  "ordered": ["yes-boss.m4a"],
  "needsYou": ["knock.wav"],
  "longDone": ["applause.mp3"],
  "subagent": [],
  "failed": ["sigh.wav"],
  "pushed": ["cheer.m4a"],
  "compacted": []
}

WAV, MP3, M4A and AIFF play. A built-in pack's name always means the built-in pack. Your clips stay on your machine: nothing is uploaded.

To add a pack to this plugin for everyone: a folder under sounds/ with the clips, a pack.json like the one above and a CREDITS.md naming the source and rights holder of every clip, then python3 scripts/packs.py, which writes the table in hooks/register.js (CI checks it is up to date).

What it reads and does

  • Reads: when a turn starts and ends, Bash commands and whether they failed, permission notifications, questions and plans waiting for you, subagent and compaction events, and your packs folder at session start. In away mode, the name of the front app (lsappinfo, no permission needed).
  • Does: plays a clip (afplay underneath), never waiting for it, so nothing slows down. Answers its own /sounds command and no other. Writes three files in ~/.config/findmutlu-sounds/: last-played (the time of the last sound), so your terminals take turns instead of talking over each other, all-terminals.json (the pack and time of the last /sounds default) and projects.json (each project folder you gave a pack, and the pack). Every terminal reads them before a sound, and asks Claude Code which folder it runs in.
  • Hooks: tool.call (Bash, AskUserQuestion, ExitPlanMode) plays a sound and passes the call on; it never changes or blocks a call. prompt.submit, turn.complete and the notification, subagent and compaction events pass through unchanged.
  • Privacy: see PRIVACY.md.
  • Never: sends anything anywhere (no network calls, no telemetry).

Install

Needs Claude Code 2.1.287 or later (mods) on macOS. Tested on 2.1.294.

/plugin marketplace add mutlumehmet/claude-plugins
/plugin install findmutlu-sounds@mehmetmutlu

A mod runs inside Claude Code with your permissions. Read the code before you install any mod.

Known gaps

  • macOS only: Claude Code plays clips with afplay. On Linux and Windows the mod loads and stays silent.
  • failed knows check steps by name (npm, pnpm, yarn, make, cargo or go with test, build, lint, typecheck or check; pytest, jest, vitest, tsc, playwright, eslint; scripts named check-, test- or lint-), so a failing script with another name stays silent.
  • Your choice of pack for one terminal (/sounds <pack>) resets when the plugin reloads; the default does not.

About

Mehmet Mutlu: work that's shipped, at mehmetmutlu.dev

Made by Mehmet Mutlu, part of claude-plugins. More of his work at mehmetmutlu.dev.

License

Code: MIT. Sounds: not covered by the MIT license. NASA clips are public domain; the game clips belong to their publishers, as each sounds/<pack>/CREDITS.md says.

Source 1 files
hooks/register.js 1637 lines
1// Sound packs for the moments that need you, and no others.
2//
3//   moment       when                                         event
4//   ordered      you sent a prompt (after /sounds rest minutes) prompt.submit
5//   needsYou     Claude waits for you: a permission prompt,   classic.Notification,
6//                a question, a plan to approve                tool.call (AskUserQuestion, ExitPlanMode)
7//   longDone     a turn of 60 s or more answered              turn.complete (main loop)
8//   subagentStart a subagent started                          classic.SubagentStart
9//   subagent     a subagent finished                          classic.SubagentStop
10//   failed       a test, build, lint or type check failed     tool.call (Bash, isError; the step run, not heredoc text)
11//   pushed       git push went through                        tool.call (Bash)
12//   compacted    the conversation was compacted               classic.PostCompact
13//   mcp          an MCP tool ran (off unless you turn it on)  tool.call (mcp__*, rests 5 minutes)
14//
15// Each moment plays the terminal's pack unless /sounds <moment> gave it its own rule: other packs,
16// one clip, random or off (kept in the plugin store as settings.moments)
17//
18// Three kinds of pack:
19//   file packs   clips in ../sounds/<pack>/ (source and rights holder in each CREDITS.md)
20//   voice packs  lines the system voice speaks ($.audio.speak), so no audio file ships (none yet)
21//   your packs   folders under ~/.config/<this plugin's name>/packs/<pack>/ with a pack.json,
22//                read at session start and played from their bytes
23//
24// Which pack plays: this terminal's (/sounds <pack>), else your default (/sounds default <pack>,
25// kept in the plugin store), else DEFAULT_PACK.
26
27const DEFAULT_PACK = 'terran'
28const LONG_TURN_MS = 60000
29const QUIET_FROM = 23
30const QUIET_TO = 7
31// One sound at a time: several subagents finishing together give one clip, not ten. The last
32// play time is shared by every terminal through a small file beside your packs folder
33const GAP_MS = 2500
34// Every prompt plays by default; /sounds rest <minutes> makes a prompt wait that long after this
35// terminal's last prompt clip, so typing away is not a clip every message
36const ORDERED_REST_MIN = 0
37const MAX_REST_MIN = 120
38
39// Front apps that mean you are already looking at Claude Code (away mode)
40const HERE_APPS = ['Terminal', 'iTerm2', 'Code', 'Visual Studio Code', 'Cursor', 'Ghostty', 'Warp', 'Claude']
41
42// Notification types that are only information, not a wait on you
43const QUIET_NOTIFICATIONS = ['idle_prompt', 'auth_success']
44
45// Checked against each step a command runs (heredoc bodies dropped), never against file text
46const CHECK_TOOL = /^(pytest|jest|vitest|tsc|playwright|eslint|mypy|ruff|shellcheck|biome)$/
47const CHECK_WORD = /^(test|tests|build|lint|typecheck|type-check|check|validate)(:.*)?$/
48const CHECK_RUNNER = /^(npm|pnpm|yarn|bun|npx|bunx|make|cargo|go|deno|gradle|mvn|swift|turbo|nx)$/
49const CHECK_SCRIPT = /(^|\/)(check|test|lint)[\w.-]*\.(sh|py|js|ts|mjs)$/
50const PUSH_COMMAND = /\bgit\s+push\b/
51// Tools that stop and wait for your answer
52const ASKING_TOOLS = ['AskUserQuestion', 'ExitPlanMode']
53
54const MOMENTS = ['ordered', 'needsYou', 'longDone', 'subagentStart', 'subagent', 'failed', 'pushed', 'compacted', 'mcp']
55
56// What each moment means, for /sounds and its replies
57const MOMENT_INFO = {
58  ordered: 'you send a prompt',
59  needsYou: 'Claude waits for your OK or answer',
60  longDone: 'a turn of 60 s or more finishes',
61  subagentStart: 'a subagent starts',
62  subagent: 'a subagent finishes',
63  failed: 'a test, build or lint fails',
64  pushed: 'git push goes through',
65  compacted: 'the conversation is compacted',
66  mcp: 'an MCP tool runs (at most every 5 minutes)',
67}
68
69// Moments few packs carry start with a rule of their own: a villager is made when a subagent
70// starts; the MCP trebuchet is off until you turn it on (MCP tools run often)
71const DEFAULT_MOMENTS = { subagentStart: { packs: ['aoe'] }, mcp: { off: true } }
72
73// Moment names are typed in lowercase (/sounds needsyou)
74const MOMENT_BY_WORD = Object.fromEntries(MOMENTS.map((m) => [m.toLowerCase(), m]))
75
76// Words people reach for first, mapped to the moment they mean
77const MOMENT_ALIASES = {
78  push: 'pushed', fail: 'failed', fails: 'failed', error: 'failed', done: 'longDone', long: 'longDone',
79  start: 'subagentStart', spawn: 'subagentStart', agent: 'subagent', compact: 'compacted',
80  prompt: 'ordered', order: 'ordered', wait: 'needsYou', ask: 'needsYou', permission: 'needsYou',
81}
82const MCP_REST_MS = 5 * 60000
83
84// FILE_PACKS_START (generated from sounds/*/pack.json by scripts/packs.py; edit those, not this)
85const FILE_PACKS = {
86  "abathur": {
87    "label": "StarCraft II: Abathur, the Zerg evolution master (fan use)",
88    "ordered": [
89      "thanks-00.m4a",
90      "thanks-01.m4a"
91    ],
92    "needsYou": [
93      "idle.m4a",
94      "help-me.m4a"
95    ],
96    "longDone": [
97      "evolution-00.m4a",
98      "evolution-01.m4a"
99    ],
100    "subagentStart": [],
101    "subagent": [
102      "brutalisk.m4a",
103      "thanks-02.m4a"
104    ],
105    "failed": [
106      "units-lost.m4a"
107    ],
108    "pushed": [
109      "positive.m4a",
110      "generic.m4a"
111    ],
112    "compacted": [
113      "brutalisk-reminder.m4a"
114    ],
115    "mcp": []
116  },
117  "aoe": {
118    "label": "Age of Empires II: wololo, the horn and the taunts (Microsoft Game Content Usage Rules)",
119    "ordered": [
120      "yes.mp3",
121      "start-the-game-already.mp3",
122      "roggan.mp3"
123    ],
124    "needsYou": [
125      "conversion-warning.mp3",
126      "what-age-are-you-in.mp3",
127      "wait-for-my-signal.mp3"
128    ],
129    "longDone": [
130      "drum-fanfare.mp3",
131      "long-time-no-siege.mp3",
132      "good-to-be-the-king.mp3",
133      "nice-town.mp3"
134    ],
135    "subagentStart": [
136      "villager-created.mp3",
137      "military-created.mp3"
138    ],
139    "subagent": [
140      "horn.mp3",
141      "sssh-ho.mp3",
142      "oooh.mp3"
143    ],
144    "failed": [
145      "herb-laugh.mp3",
146      "no.mp3",
147      "two-hours-to-die.mp3",
148      "the-wonder-no.mp3",
149      "being-rushed.mp3"
150    ],
151    "pushed": [
152      "viking-horn.mp3",
153      "converted.mp3",
154      "raiding-party.mp3"
155    ],
156    "compacted": [
157      "wololo.mp3",
158      "give-me-your-extra-resources.mp3"
159    ],
160    "mcp": [
161      "trebuchet-fire.mp3",
162      "mangonel-fire.mp3"
163    ]
164  },
165  "aoe-turk": {
166    "label": "Age of Empires II: the Turks, villagers male and female, the war cry, swords and fanfares (Microsoft Game Content Usage Rules)",
167    "ordered": [
168      "tamam.m4a",
169      "tamam-f.m4a",
170      "evet.m4a",
171      "evet-f.m4a",
172      "dogru.m4a",
173      "dogru-f.m4a",
174      "yapacagim.m4a",
175      "yapacagim-f.m4a"
176    ],
177    "needsYou": [
178      "efendim.m4a",
179      "efendim-f.m4a",
180      "emrin.m4a",
181      "emrin-f.m4a",
182      "evet-question.m4a",
183      "evet-question-f.m4a"
184    ],
185    "longDone": [
186      "hazir.m4a",
187      "hazir-f.m4a",
188      "victory.mp3",
189      "wonder.mp3"
190    ],
191    "subagentStart": [],
192    "subagent": [
193      "oduncu.m4a",
194      "oduncu-f.m4a",
195      "ciftci.m4a",
196      "ciftci-f.m4a",
197      "madenci.m4a",
198      "madenci-f.m4a",
199      "balikci.m4a",
200      "balikci-f.m4a",
201      "avci.m4a",
202      "avci-f.m4a",
203      "seyis.m4a",
204      "seyis-f.m4a"
205    ],
206    "failed": [
207      "villager-death.m4a",
208      "villager-death-f.m4a",
209      "tamirci.m4a",
210      "tamirci-f.m4a",
211      "sword-hit.mp3"
212    ],
213    "pushed": [
214      "saldir.m4a",
215      "saldir-f.m4a",
216      "allah-allah.mp3",
217      "ileri.mp3",
218      "sword-clash.mp3"
219    ],
220    "compacted": [
221      "usta.m4a",
222      "usta-f.m4a",
223      "castle.mp3"
224    ],
225    "mcp": []
226  },
227  "blood": {
228    "label": "Blood: Caleb laughs, quotes and boomsticks (fan use)",
229    "ordered": [
230      "lets-go.mp3",
231      "alright.mp3"
232    ],
233    "needsYou": [
234      "come-out.mp3",
235      "play-with-me.mp3"
236    ],
237    "longDone": [
238      "i-won.mp3",
239      "did-it.mp3"
240    ],
241    "subagentStart": [],
242    "subagent": [
243      "laughing.mp3",
244      "yeah.mp3"
245    ],
246    "failed": [
247      "all-dead.mp3",
248      "rip.mp3",
249      "pathetic.mp3"
250    ],
251    "pushed": [
252      "boomstick.mp3",
253      "napalm.mp3"
254    ],
255    "compacted": [
256      "live-again.mp3",
257      "nevermore.mp3"
258    ],
259    "mcp": []
260  },
261  "cabal": {
262    "label": "Tiberian Sun: CABAL, the Nod AI (fan use)",
263    "ordered": [
264      "establishing-battlefield-control.m4a",
265      "building.m4a",
266      "please-stand-by.m4a"
267    ],
268    "needsYou": [
269      "incoming-transmission.m4a",
270      "select-target.m4a",
271      "new-construction-options.m4a"
272    ],
273    "longDone": [
274      "primary-objective-achieved.m4a",
275      "mission-accomplished.m4a"
276    ],
277    "subagentStart": [],
278    "subagent": [
279      "reinforcements-have-arrived.m4a",
280      "objective-complete.m4a"
281    ],
282    "failed": [
283      "base-perimeter-breached.m4a",
284      "critical-unit-lost.m4a",
285      "you-make-this-easy.m4a"
286    ],
287    "pushed": [
288      "congratulations-on-your-success.m4a",
289      "prepare-for-decimation.m4a"
290    ],
291    "compacted": [
292      "construct-more-power-plants.m4a",
293      "low-power.m4a",
294      "silos-needed.m4a"
295    ],
296    "mcp": []
297  },
298  "counterstrike": {
299    "label": "Counter-Strike: the radio calls (fan use)",
300    "ordered": [
301      "lets-go.m4a",
302      "roger-that.m4a",
303      "affirmative.m4a"
304    ],
305    "needsYou": [
306      "need-backup.m4a",
307      "report-in.m4a"
308    ],
309    "longDone": [
310      "area-clear.m4a"
311    ],
312    "subagentStart": [],
313    "subagent": [
314      "moving-out.m4a",
315      "you-take-the-point.m4a"
316    ],
317    "failed": [
318      "negative.m4a",
319      "enemy-spotted.m4a"
320    ],
321    "pushed": [
322      "lock-and-load.m4a"
323    ],
324    "compacted": [
325      "fall-back.m4a",
326      "stick-together.m4a",
327      "hold-your-position.m4a"
328    ],
329    "mcp": []
330  },
331  "diablo": {
332    "label": "Diablo II effects: level up, quest done, gold, a portal opens (Blizzard, fan use)",
333    "ordered": [
334      "potion-drink.mp3",
335      "select.mp3"
336    ],
337    "needsYou": [
338      "hostile.mp3",
339      "waypoint.mp3"
340    ],
341    "longDone": [
342      "level-up.mp3",
343      "quest-done.mp3"
344    ],
345    "subagentStart": [],
346    "subagent": [
347      "gold.mp3",
348      "rune.mp3"
349    ],
350    "failed": [
351      "item-broken.mp3",
352      "glass-break.mp3"
353    ],
354    "pushed": [
355      "portal-open.mp3",
356      "cairn-success.mp3"
357    ],
358    "compacted": [
359      "identify.mp3",
360      "scroll.mp3"
361    ],
362    "mcp": []
363  },
364  "duke": {
365    "label": "Duke Nukem 3D: come get some, hail to the king, baby (fan use)",
366    "ordered": [
367      "come-get-some.mp3",
368      "im-going-in.mp3"
369    ],
370    "needsYou": [
371      "waiting-for-christmas.mp3",
372      "you-wanna-dance.mp3"
373    ],
374    "longDone": [
375      "damn-im-good.mp3",
376      "piece-of-cake.mp3"
377    ],
378    "subagentStart": [],
379    "subagent": [
380      "groovy.mp3",
381      "back-to-work.mp3"
382    ],
383    "failed": [
384      "game-over.mp3",
385      "this-sucks.mp3",
386      "gotta-hurt.mp3"
387    ],
388    "pushed": [
389      "hail-to-the-king.mp3",
390      "bubble-gum.mp3"
391    ],
392    "compacted": [
393      "much-better.mp3",
394      "needed-that.mp3"
395    ],
396    "mcp": []
397  },
398  "engineer": {
399    "label": "Team Fortress 2: the Engineer (fan use)",
400    "ordered": [
401      "teleporter-comin-right-up.mp3",
402      "buildin-a-sentry.mp3",
403      "lets-do-this-texas-style.mp3"
404    ],
405    "needsYou": [
406      "not-on-auto-pilot.mp3",
407      "help-me.mp3"
408    ],
409    "longDone": [
410      "done-and-done.mp3",
411      "knife-fight-in-a-phonebooth.mp3"
412    ],
413    "subagentStart": [],
414    "subagent": [
415      "another-satisfied-customer.mp3",
416      "nice-work.mp3"
417    ],
418    "failed": [
419      "you-just-aint-doin-it-right.mp3",
420      "sentry-down.mp3",
421      "told-ya-dont-touch.mp3"
422    ],
423    "pushed": [
424      "yeehaw.mp3",
425      "move-em-out.mp3"
426    ],
427    "compacted": [
428      "good-night-irene.mp3",
429      "now-ive-seen-everything.mp3"
430    ],
431    "mcp": []
432  },
433  "lebowski": {
434    "label": "The Big Lebowski: the Dude, Walter, Donny and friends, clean lines (fan use)",
435    "ordered": [
436      "the-dude-abides.mp3",
437      "far-out.mp3",
438      "no-problemo.mp3"
439    ],
440    "needsYou": [
441      "phones-ringing-dude.mp3",
442      "whats-your-point.mp3",
443      "certain-things-have-come-to-light.mp3"
444    ],
445    "longDone": [
446      "rug-tied-the-room-together.mp3",
447      "goodnight-sweet-prince.mp3"
448    ],
449    "subagentStart": [],
450    "subagent": [
451      "i-like-your-style.mp3",
452      "ive-got-information.mp3"
453    ],
454    "failed": [
455      "over-the-line.mp3",
456      "mark-it-zero.mp3",
457      "world-of-pain.mp3",
458      "this-aggression-will-not-stand.mp3"
459    ],
460    "pushed": [
461      "way-to-go-donny.mp3",
462      "wave-of-the-future.mp3"
463    ],
464    "compacted": [
465      "just-take-it-easy.mp3",
466      "strikes-and-gutters.mp3"
467    ],
468    "mcp": []
469  },
470  "lich": {
471    "label": "Warcraft III: the Lich and Kel'Thuzad (fan use)",
472    "ordered": [
473      "as-the-master-commands.m4a",
474      "i-shall-obey.m4a",
475      "direct-my-hatred.m4a"
476    ],
477    "needsYou": [
478      "thy-bidding-master.m4a",
479      "what-is-thy-will.m4a",
480      "i-await-your-command.m4a"
481    ],
482    "longDone": [
483      "thy-will-be-done.m4a",
484      "the-ancient-evil-survives.m4a"
485    ],
486    "subagentStart": [],
487    "subagent": [
488      "for-the-lich-king.m4a",
489      "it-is-destined.m4a"
490    ],
491    "failed": [
492      "aaugh.m4a",
493      "do-not-try-my-patience.m4a"
494    ],
495    "pushed": [
496      "kneel-before-my-frost-nova.m4a",
497      "dread-lord-not-drug-lord.m4a"
498    ],
499    "compacted": [
500      "the-scourge-will-consume-all.m4a",
501      "bone-to-pick.m4a"
502    ],
503    "mcp": []
504  },
505  "meeseeks": {
506    "label": "Rick and Morty: Mr. Meeseeks, look at me! (fan use)",
507    "ordered": [
508      "can-do.mp3",
509      "can-do-2.mp3",
510      "ooh-okay.mp3",
511      "okay.mp3"
512    ],
513    "needsYou": [
514      "what-do-you-got.mp3",
515      "excuse-me.mp3",
516      "back-to-the-task.mp3"
517    ],
518    "longDone": [
519      "all-done.mp3",
520      "exist-this-long.mp3"
521    ],
522    "subagentStart": [],
523    "subagent": [
524      "im-mr-meeseeks.mp3",
525      "look-at-me.mp3",
526      "fulfill-my-purpose.mp3",
527      "yes-sirree.mp3"
528    ],
529    "failed": [
530      "cant-take-it.mp3",
531      "frustrating.mp3",
532      "doesnt-work-like-that.mp3"
533    ],
534    "pushed": [
535      "ooh-yeah.mp3",
536      "yes-maam.mp3"
537    ],
538    "compacted": [
539      "gotta-relax.mp3",
540      "let-me-try.mp3"
541    ],
542    "mcp": []
543  },
544  "nasa": {
545    "label": "NASA radio: the Eagle has landed, liftoff, we have had a problem (public domain)",
546    "ordered": [
547      "roger-roll.m4a",
548      "quindar-beep.m4a"
549    ],
550    "needsYou": [
551      "houston-discovery.m4a",
552      "how-do-you-read.m4a"
553    ],
554    "longDone": [
555      "eagle-landed.m4a",
556      "wheels-stop.m4a"
557    ],
558    "subagentStart": [],
559    "subagent": [
560      "go-for-deploy.m4a",
561      "go-at-throttle-up.m4a"
562    ],
563    "failed": [
564      "weve-had-a-problem.m4a"
565    ],
566    "pushed": [
567      "liftoff.m4a",
568      "liftoff-41d.m4a"
569    ],
570    "compacted": [
571      "vector-transfer.m4a"
572    ],
573    "mcp": []
574  },
575  "necromancer": {
576    "label": "Diablo II Necromancer: raise skeleton, not enough mana, I cannot carry any more (Blizzard, fan use)",
577    "ordered": [
578      "ok.mp3",
579      "yes.mp3",
580      "lets-go.mp3"
581    ],
582    "needsYou": [
583      "need-help.mp3",
584      "yes-question.mp3",
585      "what-else.mp3"
586    ],
587    "longDone": [
588      "diablo-killed.mp3",
589      "baal-defeated.mp3",
590      "andariel-done.mp3"
591    ],
592    "subagentStart": [],
593    "subagent": [
594      "raise-skeleton.mp3",
595      "golem.mp3",
596      "revive.mp3"
597    ],
598    "failed": [
599      "i-cant.mp3",
600      "oops.mp3",
601      "not-enough-mana.mp3"
602    ],
603    "pushed": [
604      "time-to-die.mp3",
605      "meet-your-fate.mp3",
606      "corpse-explosion.mp3"
607    ],
608    "compacted": [
609      "cant-carry.mp3",
610      "need-mana.mp3"
611    ],
612    "mcp": []
613  },
614  "peon": {
615    "label": "Warcraft III peon: work work, zug zug, something need doing? (Blizzard, fan use)",
616    "ordered": [
617      "work-work.mp3",
618      "okey-dokey.mp3",
619      "i-can-do-that.mp3",
620      "be-happy-to.mp3"
621    ],
622    "needsYou": [
623      "something-need-doing.mp3",
624      "ready-to-work.mp3",
625      "what-do-you-want.mp3"
626    ],
627    "longDone": [
628      "zug-zug.mp3"
629    ],
630    "subagentStart": [],
631    "subagent": [
632      "me-busy.mp3",
633      "yes.mp3",
634      "ok.mp3"
635    ],
636    "failed": [
637      "splat.mp3",
638      "tummy-feels-funny.mp3",
639      "not-that-kind-of-orc.mp3"
640    ],
641    "pushed": [
642      "kill-them.mp3",
643      "why-not.mp3"
644    ],
645    "compacted": [
646      "leave-me-alone.mp3",
647      "no-time-for-play.mp3"
648    ],
649    "mcp": []
650  },
651  "protoss": {
652    "label": "StarCraft Protoss: construct additional pylons, my life for Aiur (Blizzard, fan use)",
653    "ordered": [
654      "adun-toridas.mp3"
655    ],
656    "needsYou": [
657      "transmission.mp3"
658    ],
659    "longDone": [
660      "upgrade-complete.mp3",
661      "research.mp3"
662    ],
663    "subagentStart": [],
664    "subagent": [
665      "upgrade.mp3",
666      "systems-functional.mp3"
667    ],
668    "failed": [
669      "pylons.mp3",
670      "pylons-2.mp3"
671    ],
672    "pushed": [
673      "life-for-aiur.mp3",
674      "power-overwhelming.mp3"
675    ],
676    "compacted": [
677      "vespene-gas.mp3"
678    ],
679    "mcp": []
680  },
681  "red-alert": {
682    "label": "Red Alert 2: Kirov reporting, for mother Russia (EA, fan use)",
683    "ordered": [
684      "acknowledged.mp3",
685      "yes-commander.mp3",
686      "can-do.mp3"
687    ],
688    "needsYou": [
689      "commander.mp3",
690      "give-me-a-job.mp3",
691      "anytime-boss.mp3"
692    ],
693    "longDone": [
694      "kirov-reporting.mp3",
695      "cha-ching.mp3"
696    ],
697    "subagentStart": [],
698    "subagent": [
699      "unit-reporting.mp3",
700      "agent-ready.mp3"
701    ],
702    "failed": [
703      "mommy.mp3",
704      "bail-out.mp3"
705    ],
706    "pushed": [
707      "kaboom.mp3",
708      "for-mother-russia.mp3"
709    ],
710    "compacted": [
711      "your-mind-is-clear.mp3",
712      "deconstructing.mp3"
713    ],
714    "mcp": []
715  },
716  "rick-and-morty": {
717    "label": "Rick and Morty: family-friendly lines from Rick and the crew (fan use)",
718    "ordered": [
719      "wubba-lubba-dub-dub.mp3",
720      "here-we-go.mp3",
721      "time-for-action.mp3",
722      "twenty-minute-adventure.mp3"
723    ],
724    "needsYou": [
725      "show-me-what-you-got.mp3",
726      "yes-or-no.mp3",
727      "hey-morty.mp3"
728    ],
729    "longDone": [
730      "pickle-rick.mp3",
731      "pure-luck.mp3"
732    ],
733    "subagentStart": [],
734    "subagent": [
735      "what-is-my-purpose.mp3",
736      "everything-you-want.mp3",
737      "oh-yeah.mp3"
738    ],
739    "failed": [
740      "oh-jeeze.mp3",
741      "oh-man.mp3",
742      "not-in-control.mp3"
743    ],
744    "pushed": [
745      "im-pickle-rick.mp3",
746      "my-plan-all-along.mp3"
747    ],
748    "compacted": [
749      "brain-functionality.mp3",
750      "super-intelligence.mp3"
751    ],
752    "mcp": []
753  },
754  "rick-and-morty-uncut": {
755    "label": "Rick and Morty, uncut: the swearing lines (explicit language, fan use)",
756    "ordered": [
757      "son-of-a-bitch-im-in.mp3",
758      "gonna-fucking-do-it.mp3",
759      "doctor-who.mp3"
760    ],
761    "needsYou": [
762      "who-the-fuck-are-you.mp3",
763      "seeds-up-your-butt.mp3"
764    ],
765    "longDone": [
766      "having-a-party.mp3"
767    ],
768    "subagentStart": [],
769    "subagent": [
770      "god-damn-it.mp3"
771    ],
772    "failed": [
773      "oh-fuck-me.mp3",
774      "fuck-the-government.mp3"
775    ],
776    "pushed": [
777      "everybody-fuck-off.mp3",
778      "dick-move.mp3"
779    ],
780    "compacted": [
781      "traumatized-for-breakfast.mp3",
782      "take-a-shit.mp3"
783    ],
784    "mcp": []
785  },
786  "sheogorath": {
787    "label": "The Elder Scrolls IV: Sheogorath, Prince of Madness (fan use)",
788    "ordered": [
789      "good-choice.m4a",
790      "i-like-it.m4a",
791      "dance-sing-smile.m4a"
792    ],
793    "needsYou": [
794      "ive-been-waiting-for-you.m4a",
795      "need-a-champion.m4a"
796    ],
797    "longDone": [
798      "cheese-for-everyone.m4a"
799    ],
800    "subagentStart": [],
801    "subagent": [
802      "congratulations.m4a",
803      "but-youre-not-done-yet.m4a"
804    ],
805    "failed": [
806      "boring-boring-boring.m4a",
807      "absolutely-not.m4a",
808      "how-rude.m4a"
809    ],
810    "pushed": [
811      "prince-of-madness.m4a",
812      "dont-get-too-comfortable.m4a"
813    ],
814    "compacted": [
815      "a-little-busy-here.m4a",
816      "niggling-little-details.m4a"
817    ],
818    "mcp": []
819  },
820  "stronghold": {
821    "label": "Stronghold Crusader: the AI lords (fan use)",
822    "ordered": [
823      "richard-i-march.mp3",
824      "abbot-agreed.mp3",
825      "marshal-off-we-go.mp3",
826      "philip-on-my-honor.mp3"
827    ],
828    "needsYou": [
829      "abbot-send-help.mp3",
830      "sheriff-help-surrounded.mp3"
831    ],
832    "longDone": [
833      "richard-sound-the-fanfare.mp3",
834      "abbot-victory-for-me.mp3",
835      "pig-no-one-beats-the-pig.mp3"
836    ],
837    "subagentStart": [],
838    "subagent": [
839      "rat-plans-worked-perfectly.mp3",
840      "saladin-another-has-fallen.mp3",
841      "emir-this-is-easy.mp3"
842    ],
843    "failed": [
844      "abbot-my-troops-fail-me.mp3",
845      "emir-what-a-disaster.mp3",
846      "marshal-bad-tactics.mp3"
847    ],
848    "pushed": [
849      "richard-the-lion-roars.mp3",
850      "wolf-i-win-you-lose.mp3",
851      "sheriff-sweet-victory.mp3"
852    ],
853    "compacted": [
854      "sheriff-not-father-christmas.mp3",
855      "richard-lean-times.mp3"
856    ],
857    "mcp": []
858  },
859  "tauren": {
860    "label": "World of Warcraft: the Tauren (fan use)",
861    "ordered": [
862      "peace-and-patience.m4a",
863      "indeed.m4a",
864      "it-is-so.m4a"
865    ],
866    "needsYou": [
867      "i-need-assistance.m4a",
868      "wont-you-help.m4a"
869    ],
870    "longDone": [
871      "fate-smiles-upon-you.m4a",
872      "our-ancestors-be-praised.m4a"
873    ],
874    "subagentStart": [],
875    "subagent": [
876      "well-done.m4a",
877      "pleasure-doing-business.m4a"
878    ],
879    "failed": [
880      "impermissible.m4a",
881      "i-cannot-do-that.m4a"
882    ],
883    "pushed": [
884      "for-the-horde.m4a",
885      "ride-the-winds.m4a"
886    ],
887    "compacted": [
888      "my-inventory-is-full.m4a",
889      "i-cannot-carry-more.m4a",
890      "moo.m4a"
891    ],
892    "mcp": []
893  },
894  "terran": {
895    "label": "StarCraft Terran: SCV good to go, nuclear launch detected (Blizzard, fan use)",
896    "ordered": [
897      "scv-good-to-go.mp3",
898      "jacked-up.mp3"
899    ],
900    "needsYou": [
901      "piece-of-me.mp3",
902      "supply-depots.mp3"
903    ],
904    "longDone": [
905      "battlecruiser-operational.mp3"
906    ],
907    "subagentStart": [],
908    "subagent": [
909      "research-complete.mp3",
910      "upgrade-complete.mp3"
911    ],
912    "failed": [
913      "nuclear-launch.mp3",
914      "base-under-attack.mp3"
915    ],
916    "pushed": [
917      "in-the-pipe.mp3"
918    ],
919    "compacted": [
920      "not-enough-minerals.mp3",
921      "vespene-gas.mp3"
922    ],
923    "mcp": []
924  },
925  "yuri": {
926    "label": "Red Alert 2: Yuri's Revenge: Yuri (fan use)",
927    "ordered": [
928      "your-thoughts-are-mine.m4a",
929      "a-trivial-matter.m4a",
930      "yes-i-know.m4a"
931    ],
932    "needsYou": [
933      "i-foresaw-this-need.m4a",
934      "share-your-thoughts.m4a",
935      "something-to-think-about.m4a"
936    ],
937    "longDone": [
938      "yes-my-exquisite-mind.m4a",
939      "they-will-obey.m4a"
940    ],
941    "subagentStart": [],
942    "subagent": [
943      "another-pawn-joins-us.m4a",
944      "he-shall-serve-me-well.m4a"
945    ],
946    "failed": [
947      "i-did-not-foresee-this.m4a",
948      "this-could-be-a-problem.m4a"
949    ],
950    "pushed": [
951      "there-is-only-one-yuri.m4a",
952      "yield-to-me.m4a"
953    ],
954    "compacted": [
955      "all-in-the-mind.m4a",
956      "i-cannot-be-overcome.m4a"
957    ],
958    "mcp": []
959  }
960}
961// FILE_PACKS_END
962
963const VOICE_PACKS = {}
964
965// Module variables are this terminal's: each terminal is its own process. A plugin reload
966// starts them over, which only costs this terminal's /sounds <pack> choice.
967// pack and packAt: this terminal's /sounds <pack>; all: the last /sounds default <pack> of any terminal;
968// project: the folder this session runs in and the pack /sounds project gave it (or its parent)
969const here = { pack: undefined, packAt: -1, all: undefined, cwd: '', project: undefined, playedAt: 0, orderedAt: 0, yourPacks: {}, lastClip: {} }
970
971const DEFAULTS = { isOn: true, defaultPack: DEFAULT_PACK, mode: 'always', isNightQuiet: false, restMin: ORDERED_REST_MIN }
972
973export function register(on) {
974  on('session.start', async ($, e, next) => {
975    await $.command.register({
976      name: 'sounds',
977      description: 'Sound packs for the moments that need you: /sounds to see every moment and command, /sounds <pack>, /sounds <moment> <pack>, test',
978      argumentHint: '[<pack>|default <pack>|<moment> <pack> [clip]|<moment> random|off|reset|on|off|test [pack] [moment]]',
979    })
980    here.yourPacks = await loadYourPacks($)
981    // Said once, so the way to change the pack is never a secret
982    try {
983      if (!(await $.store.get('welcomed'))) {
984        await $.store.set('welcomed', true)
985        const settings = await readSettings($)
986        $.ui.toast('Sounds: ' + settings.defaultPack + ' pack. Type /sounds for the others')
987      }
988    } catch {
989      // A hint is never worth an error line
990    }
991    return next(e)
992  })
993
994  on('command.run', { command: 'sounds' }, async ($, e) => {
995    const args = String(e.args ?? '').trim().toLowerCase().split(/\s+/).filter(Boolean)
996    const settings = await readSettings($)
997    const current = currentPack(settings)
998
999    if (args[0] === 'test') {
1000      // /sounds test [pack] [moment], either may be left out
1001      const words = args.slice(1)
1002      const moment = MOMENT_BY_WORD[words[words.length - 1]]
1003      if (moment) words.pop()
1004      const name = words[0] ?? current
1005      if (!packOf(name)) {
1006        const meant = closestPack(name)
1007        return { text: 'No pack named ' + name + (meant ? '. Did you mean ' + meant + '? /sounds test ' + meant + (moment ? ' ' + moment.toLowerCase() : '') : '\n' + listing()) }
1008      }
1009      const count = await playAll($, name, moment)
1010      return { text: 'Played ' + count + ' sounds of ' + name + (moment ? ' for ' + moment : '') }
1011    }
1012    // No word: where things stand, then every command and pack
1013    if (!args.length) return { text: describe(settings) }
1014    // /sounds <moment> ...: that moment's own rule
1015    if (MOMENT_BY_WORD[args[0]]) return momentCommand($, settings, MOMENT_BY_WORD[args[0]], args.slice(1))
1016    // A change answers with one line saying what changed, not the whole list again
1017    if (packOf(args[0]) && args.length === 1) {
1018      here.pack = args[0]
1019      here.packAt = await $.clock.now()
1020      const def = defaultOf(settings)
1021      return {
1022        text: 'This terminal now plays ' + args[0] + ': ' + packOf(args[0]).label +
1023          (args[0] === def ? '' : '\nOther terminals keep theirs (/sounds default ' + args[0] + ' switches them all)'),
1024      }
1025    }
1026    if (args[0] === 'default' && !packOf(args[1])) {
1027      const meant = args[1] && closestPack(args[1])
1028      if (meant) return { text: 'No pack named ' + args[1] + '. Did you mean ' + meant + '? /sounds default ' + meant }
1029      return { text: (args[1] ? 'No pack named ' + args[1] : 'Which pack? /sounds default <pack>') + '\n' + listing() }
1030    }
1031    // /sounds project [<pack>|off]: every terminal in this folder, or one below it
1032    if (args[0] === 'project') return projectCommand($, args.slice(1))
1033    // /sounds default <pack>: every open terminal, of both accounts, and every new one
1034    if (args[0] === 'default' && args.length === 2) {
1035      const name = args[1]
1036      if (current === name && defaultOf(settings) === name) return { text: 'Default is already ' + name + ': ' + packOf(name).label }
1037      const at = await $.clock.now()
1038      here.all = { pack: name, at }
1039      here.pack = undefined
1040      here.packAt = -1
1041      await $.fs.write(await allFile($), JSON.stringify(here.all)).catch(() => {})
1042      await $.store.set('settings', { ...settings, defaultPack: name, defaultAt: at })
1043      const kept = Object.keys(await readProjects($)).length
1044      return {
1045        text: 'Default changed: every terminal, open or new, now plays ' + name + ': ' + packOf(name).label +
1046          (kept ? '\nProject packs stay (/sounds project to see them)' : ''),
1047      }
1048    }
1049    const change = CHANGES.find((c) => c.matches(args))
1050    if (!change && args[0] === 'rest') return { text: 'How many minutes? /sounds rest <0 to ' + MAX_REST_MIN + '> (0: every prompt). Now: ' + restText(settings) }
1051    // A moment name mistyped or shortened (push, needsyu) gets the moment it meant
1052    const moment = !change && !packOf(args[0]) && closestMoment(args[0])
1053    if (moment) {
1054      return { text: 'No moment named ' + args[0] + '. Did you mean ' + moment + '? /sounds ' + [moment.toLowerCase(), ...args.slice(1)].join(' ') }
1055    }
1056    if (!change) {
1057      // A typo of a pack name (aeo-turk) gets the pack it meant, not just the list
1058      const last = args[args.length - 1]
1059      const meant = !packOf(last) && closestPack(last)
1060      if (meant) return { text: 'No pack named ' + last + '. Did you mean ' + meant + '? /sounds ' + (args[0] === 'default' ? 'default ' : '') + meant }
1061      return { text: 'Unknown: ' + args.join(' ') + '\n' + commands() + '\n' + listing() }
1062    }
1063    const updated = change.apply({ ...settings }, args)
1064    await $.store.set('settings', updated)
1065    return { text: change.said(updated, args, settings) }
1066  })
1067
1068  // An order given: the unit answers. You are at the keyboard, so away mode does not apply
1069  on('prompt.submit', async ($, e, next) => {
1070    // The rest counts from this terminal's last prompt clip only: a clip of another moment, or of
1071    // another terminal, does not silence the next prompt
1072    const settings = await readSettings($).catch(() => DEFAULTS)
1073    const now = await $.clock.now()
1074    if (here.orderedAt && now - here.orderedAt < restOf(settings) * 60000) return next(e)
1075    if (await cue($, 'ordered', { isPresent: true })) here.orderedAt = now
1076    return next(e)
1077  })
1078
1079  on('classic.Notification', async ($, e, next) => {
1080    if (!QUIET_NOTIFICATIONS.includes(e.notification_type)) await cue($, 'needsYou')
1081    return next(e)
1082  })
1083
1084  on('classic.SubagentStart', async ($, e, next) => {
1085    await cue($, 'subagentStart').catch(() => {})
1086    return next(e)
1087  })
1088
1089  on('classic.SubagentStop', async ($, e, next) => {
1090    await cue($, 'subagent')
1091    return next(e)
1092  })
1093
1094  on('classic.PostCompact', async ($, e, next) => {
1095    await cue($, 'compacted')
1096    return next(e)
1097  })
1098
1099  on('turn.complete', async ($, e, next) => {
1100    const out = await next(e)
1101    if (!e.agentId && e.reason === 'answer' && e.durationMs >= LONG_TURN_MS) await cue($, 'longDone')
1102    return out
1103  })
1104
1105  // One hook for the tools: a plugin gets one hook per event
1106  on('tool.call', async ($, e, next) => {
1107    // A question waits for you, so the sound goes before the answer, not after it
1108    if (ASKING_TOOLS.includes(e.tool)) {
1109      await cue($, 'needsYou')
1110      return next(e)
1111    }
1112    if (String(e.tool).startsWith('mcp__')) {
1113      const out = await next(e)
1114      if (out.deny === undefined && !out.isError) await cue($, 'mcp', { restMs: MCP_REST_MS })
1115      return out
1116    }
1117    if (e.tool !== 'Bash') return next(e)
1118    const out = await next(e)
1119    const command = String(e.command ?? '')
1120    if (out.deny === undefined) {
1121      if (out.isError && runsCheck(command)) await cue($, 'failed')
1122      else if (!out.isError && PUSH_COMMAND.test(command)) await cue($, 'pushed')
1123    }
1124    return out
1125  })
1126}
1127
1128// Plays one sound of the moment when the settings allow it; never delays or breaks the event
1129async function cue($, moment, opts = {}) {
1130  try {
1131    const settings = await readSettings($)
1132    if (!settings.isOn) return
1133    const choices = choicesFor(settings, moment)
1134    if (!choices.length) return
1135    const now = await $.clock.now()
1136    const last = Math.max(here.playedAt, await sharedPlayedAt($))
1137    if (now - last < GAP_MS) return
1138    if (settings.isNightQuiet && isQuietHour(new Date(now).getHours())) return
1139    if (!opts.isPresent && settings.mode === 'away' && (await isHere($))) return
1140    here.playedAt = now
1141    await sharePlayedAt($, now)
1142    // Not awaited: the sound plays while the session goes on
1143    const [name, item] = pick(choices.map((c) => c.pack + '/' + c.item), here.lastClip, moment, now).split(/\/(.*)/s)
1144    sound($, packOf(name), item).catch(() => {})
1145    return true
1146  } catch {
1147    // A sound is never worth an error line
1148  }
1149  return false
1150}
1151
1152// One sound of a pack: a clip of the plugin's, a line to speak, or a file of yours
1153async function sound($, pack, item) {
1154  if (pack.kind === 'voice') {
1155    try {
1156      await $.audio.speak(item, { voice: pack.voice })
1157    } catch {
1158      // The voice is not installed here: the default voice says it
1159      await $.audio.speak(item)
1160    }
1161  } else if (pack.kind === 'yours') {
1162    const { base64 } = await $.fs.read(pack.dir + '/' + item, { as: 'bytes' })
1163    await $.audio.play({ base64, mime: mimeOf(item) })
1164  } else {
1165    await $.audio.play({ asset: 'sounds/' + pack.name + '/' + item })
1166  }
1167}
1168
1169// A clip of the moment, never the one this terminal played last for it
1170function pick(choices, lastClip, key, now) {
1171  let i = Math.floor(now / 1000) % choices.length
1172  if (choices.length > 1 && choices[i] === lastClip[key]) i = (i + 1) % choices.length
1173  lastClip[key] = choices[i]
1174  return choices[i]
1175}
1176
1177// The file every terminal reads and writes the last play time through
1178async function playedAtFile($) {
1179  return (await $.env.get('HOME')) + '/.config/' + $.plugin.name + '/last-played'
1180}
1181
1182// The last /sounds default, shared by every terminal of both accounts beside the last play time
1183async function allFile($) {
1184  return (await $.env.get('HOME')) + '/.config/' + $.plugin.name + '/all-terminals.json'
1185}
1186
1187async function readAll($) {
1188  try {
1189    const all = JSON.parse(await $.fs.read(await allFile($)))
1190    return all && typeof all.pack === 'string' && Number.isFinite(all.at) ? all : undefined
1191  } catch {
1192    // Never used, or unreadable: each terminal keeps its own pack
1193    return undefined
1194  }
1195}
1196
1197// Packs per project folder, { "<folder>": "<pack>" }, shared by both accounts like the files above
1198async function projectsFile($) {
1199  return (await $.env.get('HOME')) + '/.config/' + $.plugin.name + '/projects.json'
1200}