tonton-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tonton contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # tonton
2
+
3
+ Play a notification sound from your terminal. Zero dependencies. ~15KB.
4
+
5
+ Perfect for getting pinged when AI coding tools (Claude Code, Codex, etc.) finish a task — or need your attention.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx tonton-cli --setup
11
+ ```
12
+
13
+ That's it. Claude Code will now play:
14
+ - A **done** sound when the agent finishes
15
+ - An **input** sound when the agent needs you
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install -g tonton-cli
21
+ ```
22
+
23
+ Or use without installing — `npx tonton-cli` works everywhere.
24
+
25
+ ## Two Sounds
26
+
27
+ | Command | When | macOS | Cross-platform |
28
+ |---------|------|-------|---------------|
29
+ | `tonton --done` | Agent finished | Glass | Descending chime |
30
+ | `tonton --input` | Agent needs you | Funk | Ascending chime |
31
+
32
+ ```bash
33
+ tonton --done # hear the "finished" sound
34
+ tonton --input # hear the "need attention" sound
35
+ tonton # default notification sound
36
+ ```
37
+
38
+ ## Use with AI CLI Tools
39
+
40
+ ### Claude Code — automatic setup
41
+
42
+ ```bash
43
+ npx tonton-cli --setup
44
+ ```
45
+
46
+ This adds hooks to `~/.claude/settings.json` so Claude Code plays distinct sounds when done vs. needing input. Run once, works forever.
47
+
48
+ To remove: `npx tonton-cli --remove`
49
+
50
+ ### Claude Code — manual setup
51
+
52
+ Add to `~/.claude/settings.json`:
53
+
54
+ ```json
55
+ {
56
+ "hooks": {
57
+ "AfterAssistantTurn": [
58
+ { "type": "command", "command": "npx tonton-cli --done" }
59
+ ],
60
+ "Notification": [
61
+ { "type": "command", "command": "npx tonton-cli --input" }
62
+ ]
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Codex CLI
68
+
69
+ ```bash
70
+ codex "fix the bug" ; npx tonton-cli --done
71
+ ```
72
+
73
+ ### Any command
74
+
75
+ ```bash
76
+ npm test ; npx tonton-cli --done
77
+ long-running-task ; npx tonton-cli
78
+ ```
79
+
80
+ ### Shell alias
81
+
82
+ ```bash
83
+ # Add to ~/.zshrc or ~/.bashrc
84
+ notify() { "$@"; npx tonton-cli --done; }
85
+
86
+ # Then use:
87
+ notify npm test
88
+ notify codex "refactor this"
89
+ ```
90
+
91
+ ## More Options
92
+
93
+ ```bash
94
+ # Pick a specific sound (macOS system sounds)
95
+ tonton --sound Glass
96
+ tonton --sound Hero
97
+
98
+ # Adjust volume (macOS only)
99
+ tonton --volume 0.5
100
+
101
+ # List available sounds
102
+ tonton --list
103
+ ```
104
+
105
+ ## Programmatic API
106
+
107
+ ```js
108
+ import { play, listSounds } from 'tonton-cli';
109
+
110
+ await play(); // default sound
111
+ await play({ sound: 'done' }); // completion sound
112
+ await play({ sound: 'input' }); // attention sound
113
+ await play({ sound: 'Glass' }); // macOS system sound
114
+ await play({ volume: 0.5 }); // half volume (macOS)
115
+
116
+ const sounds = listSounds(); // available sound names
117
+ ```
118
+
119
+ ## Cross-Platform
120
+
121
+ | Platform | Player | Sounds |
122
+ |----------|--------|--------|
123
+ | **macOS** | `afplay` (built-in) | 14 system sounds + synthesized chime |
124
+ | **Linux** | `paplay` / `aplay` / `ffplay` | Freedesktop sounds + synthesized chime |
125
+ | **Windows** | PowerShell SoundPlayer | Windows Media sounds + synthesized chime |
126
+
127
+ Falls back to a synthesized two-tone chime, then terminal bell — always plays something.
128
+
129
+ **Always exits 0.** A notification sound will never break your scripts, hooks, or CI.
130
+
131
+ ## License
132
+
133
+ MIT
package/bin/tonton.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { play, listSounds } = require('../lib/index');
5
+ const { version } = require('../package.json');
6
+
7
+ const args = process.argv.slice(2);
8
+ let sound = undefined;
9
+ let vol = undefined;
10
+
11
+ for (let i = 0; i < args.length; i++) {
12
+ const arg = args[i];
13
+
14
+ if (arg === '-h' || arg === '--help') {
15
+ console.log(`tonton v${version} — notification sound for your terminal
16
+
17
+ Usage: tonton [options]
18
+
19
+ Setup:
20
+ --setup Auto-configure Claude Code hooks (one-time)
21
+ --remove Remove tonton hooks from Claude Code
22
+
23
+ Presets:
24
+ -d, --done Agent finished (descending chime)
25
+ -i, --input Agent needs input (ascending chime)
26
+
27
+ Options:
28
+ -s, --sound <name> Sound to play (default: "Ping" on macOS, "chime" elsewhere)
29
+ -v, --volume <0-1> Volume level, 0.0 to 1.0 (macOS only, default: 1.0)
30
+ -l, --list List available sounds
31
+ -h, --help Show this help
32
+ --version Show version
33
+
34
+ Quick start:
35
+ npx tonton-cli --setup Configure Claude Code hooks automatically
36
+
37
+ Examples:
38
+ npx tonton-cli Play default notification sound
39
+ npx tonton-cli --done Agent finished working
40
+ npx tonton-cli --input Agent needs your attention
41
+ npx tonton-cli -s Glass Play the Glass sound (macOS)
42
+
43
+ After any command:
44
+ codex "fix the bug" ; npx tonton-cli --done`);
45
+ process.exit(0);
46
+ }
47
+
48
+ if (arg === '--version') {
49
+ console.log(version);
50
+ process.exit(0);
51
+ }
52
+
53
+ if (arg === '--setup') {
54
+ console.log('Setting up Claude Code hooks...\n');
55
+ require('../lib/setup').setup();
56
+ process.exit(0);
57
+ }
58
+
59
+ if (arg === '--remove') {
60
+ console.log('Removing tonton hooks...\n');
61
+ require('../lib/setup').unsetup();
62
+ process.exit(0);
63
+ }
64
+
65
+ if (arg === '-l' || arg === '--list') {
66
+ const sounds = listSounds();
67
+ console.log('Available sounds:');
68
+ for (const s of sounds) {
69
+ console.log(' ' + s);
70
+ }
71
+ process.exit(0);
72
+ }
73
+
74
+ if (arg === '-d' || arg === '--done') {
75
+ sound = 'done';
76
+ continue;
77
+ }
78
+
79
+ if (arg === '-i' || arg === '--input') {
80
+ sound = 'input';
81
+ continue;
82
+ }
83
+
84
+ if ((arg === '-s' || arg === '--sound') && i + 1 < args.length) {
85
+ sound = args[++i];
86
+ continue;
87
+ }
88
+
89
+ if ((arg === '-v' || arg === '--volume') && i + 1 < args.length) {
90
+ vol = parseFloat(args[++i]);
91
+ continue;
92
+ }
93
+ }
94
+
95
+ play({ sound, volume: vol }).then(() => {
96
+ process.exit(0);
97
+ }).catch(() => {
98
+ process.exit(0);
99
+ });
package/lib/index.js ADDED
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ const { resolveSound, listSounds, generateWav } = require('./sounds');
4
+ const { playSound } = require('./player');
5
+
6
+ async function play(options = {}) {
7
+ const { sound = 'default', volume } = options;
8
+
9
+ try {
10
+ const soundInfo = resolveSound(sound);
11
+ await playSound(soundInfo, { volume });
12
+ } catch {
13
+ // Never throw — a notification sound failing is not an error
14
+ try { process.stdout.write('\x07'); } catch {}
15
+ }
16
+ }
17
+
18
+ module.exports = { play, listSounds, generateWav };
package/lib/index.mjs ADDED
@@ -0,0 +1,6 @@
1
+ import { createRequire } from 'module';
2
+ const require = createRequire(import.meta.url);
3
+ const tonton = require('./index.js');
4
+
5
+ export default tonton.play;
6
+ export const { play, listSounds, generateWav } = tonton;
package/lib/player.js ADDED
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const { execFile, exec } = require('child_process');
4
+ const { execFileSync } = require('child_process');
5
+ const fs = require('fs');
6
+
7
+ const TIMEOUT = 5000;
8
+ let cachedLinuxPlayer = undefined; // undefined = not detected yet
9
+
10
+ function findLinuxPlayer() {
11
+ if (cachedLinuxPlayer !== undefined) return cachedLinuxPlayer;
12
+
13
+ const players = [
14
+ { cmd: 'paplay', args: (f) => [f] },
15
+ { cmd: 'pw-play', args: (f) => [f] },
16
+ { cmd: 'aplay', args: (f) => [f], wavOnly: true },
17
+ { cmd: 'ffplay', args: (f) => ['-nodisp', '-autoexit', '-loglevel', 'quiet', f] },
18
+ { cmd: 'play', args: (f) => ['-q', f] },
19
+ { cmd: 'mpg123', args: (f) => ['-q', f] },
20
+ ];
21
+
22
+ for (const player of players) {
23
+ try {
24
+ execFileSync('which', [player.cmd], { stdio: 'ignore' });
25
+ cachedLinuxPlayer = player;
26
+ return player;
27
+ } catch {}
28
+ }
29
+
30
+ cachedLinuxPlayer = null;
31
+ return null;
32
+ }
33
+
34
+ function cleanup(soundInfo) {
35
+ if (soundInfo.cleanup && soundInfo.filePath) {
36
+ fs.unlink(soundInfo.filePath, () => {}); // ignore errors
37
+ }
38
+ }
39
+
40
+ function playDarwin(soundInfo, options = {}) {
41
+ return new Promise((resolve) => {
42
+ const args = [soundInfo.filePath];
43
+ if (options.volume != null) {
44
+ args.push('-v', String(options.volume));
45
+ }
46
+
47
+ const child = execFile('afplay', args, { timeout: TIMEOUT }, (err) => {
48
+ cleanup(soundInfo);
49
+ resolve();
50
+ });
51
+
52
+ child.on('error', () => {
53
+ cleanup(soundInfo);
54
+ resolve();
55
+ });
56
+ });
57
+ }
58
+
59
+ function playLinux(soundInfo, options = {}) {
60
+ return new Promise((resolve) => {
61
+ const player = findLinuxPlayer();
62
+
63
+ if (!player) {
64
+ cleanup(soundInfo);
65
+ bellFallback();
66
+ resolve();
67
+ return;
68
+ }
69
+
70
+ // If player only supports wav but file isn't wav, need synthesized wav
71
+ if (player.wavOnly && !soundInfo.filePath.endsWith('.wav')) {
72
+ const { synthesizeToTemp } = require('./sounds');
73
+ cleanup(soundInfo);
74
+ soundInfo = synthesizeToTemp();
75
+ }
76
+
77
+ const args = player.args(soundInfo.filePath);
78
+ const child = execFile(player.cmd, args, { timeout: TIMEOUT }, (err) => {
79
+ cleanup(soundInfo);
80
+ resolve();
81
+ });
82
+
83
+ child.on('error', () => {
84
+ cleanup(soundInfo);
85
+ bellFallback();
86
+ resolve();
87
+ });
88
+ });
89
+ }
90
+
91
+ function playWindows(soundInfo, options = {}) {
92
+ return new Promise((resolve) => {
93
+ const escaped = soundInfo.filePath.replace(/'/g, "''");
94
+ const cmd = `powershell -Command "(New-Object System.Media.SoundPlayer '${escaped}').PlaySync()"`;
95
+
96
+ exec(cmd, { timeout: TIMEOUT }, (err) => {
97
+ cleanup(soundInfo);
98
+ if (err) {
99
+ // Fallback to Console.Beep
100
+ exec('powershell -Command "[System.Console]::Beep(880, 300)"', { timeout: TIMEOUT }, () => {
101
+ resolve();
102
+ });
103
+ } else {
104
+ resolve();
105
+ }
106
+ });
107
+ });
108
+ }
109
+
110
+ function bellFallback() {
111
+ try {
112
+ process.stdout.write('\x07');
113
+ } catch {}
114
+ }
115
+
116
+ function playSound(soundInfo, options = {}) {
117
+ const platform = process.platform;
118
+
119
+ if (platform === 'darwin') return playDarwin(soundInfo, options);
120
+ if (platform === 'linux') return playLinux(soundInfo, options);
121
+ if (platform === 'win32') return playWindows(soundInfo, options);
122
+
123
+ // Unknown platform
124
+ cleanup(soundInfo);
125
+ bellFallback();
126
+ return Promise.resolve();
127
+ }
128
+
129
+ module.exports = { playSound };
package/lib/setup.js ADDED
@@ -0,0 +1,103 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
8
+
9
+ const HOOKS = {
10
+ Notification: { type: 'command', command: 'npx tonton-cli --input' },
11
+ AfterAssistantTurn: { type: 'command', command: 'npx tonton-cli --done' },
12
+ };
13
+
14
+ function readSettings() {
15
+ try {
16
+ const raw = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8');
17
+ return JSON.parse(raw);
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ function writeSettings(settings) {
24
+ const dir = path.dirname(CLAUDE_SETTINGS_PATH);
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
27
+ }
28
+
29
+ function hookExists(hooks, newHook) {
30
+ return hooks.some(h => h.command === newHook.command);
31
+ }
32
+
33
+ function setup() {
34
+ const settings = readSettings();
35
+
36
+ if (!settings.hooks) settings.hooks = {};
37
+
38
+ let added = 0;
39
+
40
+ for (const [event, hook] of Object.entries(HOOKS)) {
41
+ if (!settings.hooks[event]) settings.hooks[event] = [];
42
+
43
+ if (!hookExists(settings.hooks[event], hook)) {
44
+ settings.hooks[event].push(hook);
45
+ added++;
46
+ console.log(` + ${event} → ${hook.command}`);
47
+ } else {
48
+ console.log(` ~ ${event} → already configured`);
49
+ }
50
+ }
51
+
52
+ if (added > 0) {
53
+ writeSettings(settings);
54
+ console.log(`\nWrote ${CLAUDE_SETTINGS_PATH}`);
55
+ }
56
+
57
+ console.log('\nDone! Claude Code will now play:');
58
+ console.log(' --done sound when the agent finishes');
59
+ console.log(' --input sound when the agent needs you');
60
+ }
61
+
62
+ function unsetup() {
63
+ const settings = readSettings();
64
+
65
+ if (!settings.hooks) {
66
+ console.log('No hooks found — nothing to remove.');
67
+ return;
68
+ }
69
+
70
+ let removed = 0;
71
+
72
+ for (const [event, hook] of Object.entries(HOOKS)) {
73
+ if (!settings.hooks[event]) continue;
74
+
75
+ const before = settings.hooks[event].length;
76
+ settings.hooks[event] = settings.hooks[event].filter(h => h.command !== hook.command);
77
+ const after = settings.hooks[event].length;
78
+
79
+ if (before !== after) {
80
+ removed++;
81
+ console.log(` - ${event} → removed`);
82
+ }
83
+
84
+ // Clean up empty arrays
85
+ if (settings.hooks[event].length === 0) {
86
+ delete settings.hooks[event];
87
+ }
88
+ }
89
+
90
+ // Clean up empty hooks object
91
+ if (Object.keys(settings.hooks).length === 0) {
92
+ delete settings.hooks;
93
+ }
94
+
95
+ if (removed > 0) {
96
+ writeSettings(settings);
97
+ console.log(`\nWrote ${CLAUDE_SETTINGS_PATH}`);
98
+ } else {
99
+ console.log('No tonton hooks found — nothing to remove.');
100
+ }
101
+ }
102
+
103
+ module.exports = { setup, unsetup };
package/lib/sounds.js ADDED
@@ -0,0 +1,248 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const MACOS_SOUNDS_DIR = '/System/Library/Sounds';
8
+ const FREEDESKTOP_DIRS = [
9
+ '/usr/share/sounds/freedesktop/stereo',
10
+ '/usr/share/sounds/gnome/default/alerts',
11
+ '/usr/share/sounds/ubuntu/stereo',
12
+ ];
13
+ const WINDOWS_SOUNDS_DIR = 'C:\\Windows\\Media';
14
+
15
+ // Presets: distinct sounds for different events
16
+ const PRESETS = {
17
+ done: {
18
+ // Descending two-tone: satisfying "complete" feel
19
+ macos: 'Glass',
20
+ synth: { tones: [{ freq: 880, start: 0 }, { freq: 660, start: 0.15 }], duration: 0.35 },
21
+ },
22
+ input: {
23
+ // Ascending two-tone: questioning "hey, look here" feel
24
+ macos: 'Funk',
25
+ synth: { tones: [{ freq: 440, start: 0 }, { freq: 880, start: 0.12 }], duration: 0.4 },
26
+ },
27
+ };
28
+
29
+ function generateWav(options = {}) {
30
+ const {
31
+ frequency = 880,
32
+ frequency2 = 1320,
33
+ duration = 0.3,
34
+ sampleRate = 8000,
35
+ volume = 0.8,
36
+ tones = null,
37
+ } = options;
38
+
39
+ const numSamples = Math.floor(sampleRate * (tones ? duration : duration));
40
+ const dataSize = numSamples * 2; // 16-bit mono
41
+ const fileSize = 44 + dataSize;
42
+
43
+ const buf = Buffer.alloc(fileSize);
44
+
45
+ // RIFF header
46
+ buf.write('RIFF', 0);
47
+ buf.writeUInt32LE(fileSize - 8, 4);
48
+ buf.write('WAVE', 8);
49
+
50
+ // fmt chunk
51
+ buf.write('fmt ', 12);
52
+ buf.writeUInt32LE(16, 16); // chunk size
53
+ buf.writeUInt16LE(1, 20); // PCM format
54
+ buf.writeUInt16LE(1, 22); // mono
55
+ buf.writeUInt32LE(sampleRate, 24);
56
+ buf.writeUInt32LE(sampleRate * 2, 28); // byte rate
57
+ buf.writeUInt16LE(2, 32); // block align
58
+ buf.writeUInt16LE(16, 34); // bits per sample
59
+
60
+ // data chunk
61
+ buf.write('data', 36);
62
+ buf.writeUInt32LE(dataSize, 40);
63
+
64
+ for (let i = 0; i < numSamples; i++) {
65
+ const t = i / sampleRate;
66
+ let sample;
67
+
68
+ if (tones) {
69
+ // Multi-tone mode: each tone starts at a given time offset
70
+ sample = 0;
71
+ for (const tone of tones) {
72
+ const tLocal = t - tone.start;
73
+ if (tLocal >= 0) {
74
+ const decay = Math.exp(-tLocal * 12);
75
+ sample += Math.sin(2 * Math.PI * tone.freq * tLocal) * decay;
76
+ }
77
+ }
78
+ sample = sample / tones.length * volume;
79
+ } else {
80
+ // Default: two simultaneous tones with decay
81
+ const decay = Math.exp(-t * 10);
82
+ const s1 = Math.sin(2 * Math.PI * frequency * t);
83
+ const s2 = Math.sin(2 * Math.PI * frequency2 * t) * 0.6;
84
+ sample = (s1 + s2) * decay * volume;
85
+ }
86
+
87
+ const value = Math.max(-1, Math.min(1, sample));
88
+ buf.writeInt16LE(Math.floor(value * 32767), 44 + i * 2);
89
+ }
90
+
91
+ return buf;
92
+ }
93
+
94
+ function synthesizePreset(presetName) {
95
+ const preset = PRESETS[presetName];
96
+ if (!preset) return null;
97
+ const wavBuf = generateWav(preset.synth);
98
+ const tmpPath = path.join(os.tmpdir(), 'tonton-' + presetName + '-' + process.pid + '.wav');
99
+ fs.writeFileSync(tmpPath, wavBuf);
100
+ return { filePath: tmpPath, cleanup: true };
101
+ }
102
+
103
+ function resolveMacosSound(name) {
104
+ if (name === 'default' || name === 'chime') name = 'Ping';
105
+
106
+ // Try exact match first
107
+ const filePath = path.join(MACOS_SOUNDS_DIR, name + '.aiff');
108
+ if (fs.existsSync(filePath)) {
109
+ return { filePath, cleanup: false };
110
+ }
111
+
112
+ // Try case-insensitive
113
+ try {
114
+ const files = fs.readdirSync(MACOS_SOUNDS_DIR);
115
+ const match = files.find(f => f.toLowerCase() === (name + '.aiff').toLowerCase());
116
+ if (match) {
117
+ return { filePath: path.join(MACOS_SOUNDS_DIR, match), cleanup: false };
118
+ }
119
+ } catch {}
120
+
121
+ return null;
122
+ }
123
+
124
+ function resolveLinuxSound(name) {
125
+ if (name !== 'default' && name !== 'chime') {
126
+ // Try to find a named sound in freedesktop dirs
127
+ for (const dir of FREEDESKTOP_DIRS) {
128
+ for (const ext of ['.oga', '.ogg', '.wav']) {
129
+ const filePath = path.join(dir, name + ext);
130
+ if (fs.existsSync(filePath)) {
131
+ return { filePath, cleanup: false };
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ // Try default freedesktop sounds
138
+ const defaults = ['complete.oga', 'bell.oga', 'message-new-instant.oga', 'message.oga'];
139
+ for (const dir of FREEDESKTOP_DIRS) {
140
+ for (const file of defaults) {
141
+ const filePath = path.join(dir, file);
142
+ if (fs.existsSync(filePath)) {
143
+ return { filePath, cleanup: false };
144
+ }
145
+ }
146
+ }
147
+
148
+ return null;
149
+ }
150
+
151
+ function resolveWindowsSound(name) {
152
+ if (name !== 'default' && name !== 'chime') {
153
+ const filePath = path.join(WINDOWS_SOUNDS_DIR, name + '.wav');
154
+ if (fs.existsSync(filePath)) {
155
+ return { filePath, cleanup: false };
156
+ }
157
+ }
158
+
159
+ const defaults = ['notify.wav', 'chimes.wav', 'tada.wav', 'Windows Notify System Generic.wav'];
160
+ for (const file of defaults) {
161
+ const filePath = path.join(WINDOWS_SOUNDS_DIR, file);
162
+ if (fs.existsSync(filePath)) {
163
+ return { filePath, cleanup: false };
164
+ }
165
+ }
166
+
167
+ return null;
168
+ }
169
+
170
+ function synthesizeToTemp() {
171
+ const wavBuf = generateWav();
172
+ const tmpPath = path.join(os.tmpdir(), 'tonton-chime-' + process.pid + '.wav');
173
+ fs.writeFileSync(tmpPath, wavBuf);
174
+ return { filePath: tmpPath, cleanup: true };
175
+ }
176
+
177
+ function resolveSound(name = 'default') {
178
+ const platform = process.platform;
179
+ const preset = PRESETS[name];
180
+
181
+ // If it's a preset (done/input), use platform-specific sound or synth
182
+ if (preset) {
183
+ if (platform === 'darwin') {
184
+ return resolveMacosSound(preset.macos) || synthesizePreset(name);
185
+ }
186
+ return synthesizePreset(name);
187
+ }
188
+
189
+ if (platform === 'darwin') {
190
+ return resolveMacosSound(name) || synthesizeToTemp();
191
+ }
192
+
193
+ if (platform === 'linux') {
194
+ return resolveLinuxSound(name) || synthesizeToTemp();
195
+ }
196
+
197
+ if (platform === 'win32') {
198
+ return resolveWindowsSound(name) || synthesizeToTemp();
199
+ }
200
+
201
+ // Unknown platform — synthesize
202
+ return synthesizeToTemp();
203
+ }
204
+
205
+ function listSounds() {
206
+ const platform = process.platform;
207
+ const sounds = [];
208
+
209
+ if (platform === 'darwin') {
210
+ try {
211
+ const files = fs.readdirSync(MACOS_SOUNDS_DIR);
212
+ for (const f of files) {
213
+ if (f.endsWith('.aiff')) {
214
+ sounds.push(f.replace('.aiff', ''));
215
+ }
216
+ }
217
+ } catch {}
218
+ } else if (platform === 'linux') {
219
+ for (const dir of FREEDESKTOP_DIRS) {
220
+ try {
221
+ const files = fs.readdirSync(dir);
222
+ for (const f of files) {
223
+ const name = f.replace(/\.(oga|ogg|wav)$/, '');
224
+ if (name !== f && !sounds.includes(name)) {
225
+ sounds.push(name);
226
+ }
227
+ }
228
+ } catch {}
229
+ }
230
+ } else if (platform === 'win32') {
231
+ try {
232
+ const files = fs.readdirSync(WINDOWS_SOUNDS_DIR);
233
+ for (const f of files) {
234
+ if (f.endsWith('.wav')) {
235
+ sounds.push(f.replace('.wav', ''));
236
+ }
237
+ }
238
+ } catch {}
239
+ }
240
+
241
+ if (!sounds.includes('chime')) {
242
+ sounds.push('chime');
243
+ }
244
+
245
+ return sounds.sort();
246
+ }
247
+
248
+ module.exports = { resolveSound, listSounds, generateWav };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "tonton-cli",
3
+ "version": "1.0.0",
4
+ "description": "Play a notification sound from the CLI. Zero dependencies. Perfect for AI coding tool hooks.",
5
+ "keywords": [
6
+ "notification",
7
+ "sound",
8
+ "beep",
9
+ "alert",
10
+ "cli",
11
+ "claude-code",
12
+ "codex",
13
+ "hook",
14
+ "bell",
15
+ "ding"
16
+ ],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/kyrolloszakaria/tonton.git"
21
+ },
22
+ "homepage": "https://github.com/kyrolloszakaria/tonton#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/kyrolloszakaria/tonton/issues"
25
+ },
26
+ "bin": {
27
+ "tonton": "bin/tonton.js"
28
+ },
29
+ "main": "./lib/index.js",
30
+ "exports": {
31
+ ".": {
32
+ "import": "./lib/index.mjs",
33
+ "require": "./lib/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "bin/",
38
+ "lib/",
39
+ "LICENSE",
40
+ "README.md"
41
+ ],
42
+ "engines": {
43
+ "node": ">=14.0.0"
44
+ },
45
+ "scripts": {
46
+ "test": "node test/test.js"
47
+ }
48
+ }