vibe-notify 0.1.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 vibe-notify
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,112 @@
1
+ # vibe-notify 🔊
2
+
3
+ Notification sounds for Claude Code sessions. Plays a customizable beep/jingle when Claude finishes responding — so you can step away and still know when your task is done.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install globally
9
+ npm install -g vibe-notify
10
+
11
+ # Test it
12
+ vibe-notify victory
13
+
14
+ # Set up config
15
+ vibe-notify --init
16
+ ```
17
+
18
+ Or run without installing:
19
+
20
+ ```bash
21
+ npx -y vibe-notify victory
22
+ ```
23
+
24
+ ## Built-in Sounds
25
+
26
+ | Sound | Description |
27
+ |-------|-------------|
28
+ | `short-beep` | Single short beep — quick acknowledgment |
29
+ | `double-beep` | Two quick beeps — "something finished" |
30
+ | `ascending` | Three rising tones — positive progression |
31
+ | `descending` | Three falling tones — winding down |
32
+ | `victory` | C-E-G-C arpeggio — celebratory jingle |
33
+ | `custom` | Play your own `.wav` file |
34
+
35
+ List them anytime: `vibe-notify --list`
36
+
37
+ ## Configuration
38
+
39
+ Config file location (checked in order):
40
+ 1. **Project-local**: `.vibe-notify.json` in your project root
41
+ 2. **Global**: `~/.vibe-notify/config.json`
42
+
43
+ ```json
44
+ {
45
+ "sound": "ascending",
46
+ "customFile": null,
47
+ "volume": 0.7,
48
+ "playback": {
49
+ "method": "auto"
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### Custom Sound File
55
+
56
+ ```json
57
+ {
58
+ "sound": "custom",
59
+ "customFile": "C:\\Users\\You\\sounds\\done.wav",
60
+ "volume": 1.0
61
+ }
62
+ ```
63
+
64
+ ## Claude Code Hook Setup
65
+
66
+ Add this to your `.claude/settings.json` (project or global) to auto-play when Claude finishes:
67
+
68
+ ```json
69
+ {
70
+ "hooks": {
71
+ "Stop": [
72
+ {
73
+ "hooks": [
74
+ {
75
+ "type": "command",
76
+ "command": "npx",
77
+ "args": ["-y", "vibe-notify"],
78
+ "timeout": 15
79
+ }
80
+ ]
81
+ }
82
+ ]
83
+ }
84
+ }
85
+ ```
86
+
87
+ Replace `"npx", "-y", "vibe-notify"` with `"vibe-notify"` if installed globally. Add a sound name like `"victory"` to the args to override the config.
88
+
89
+ ## CLI Reference
90
+
91
+ ```
92
+ vibe-notify [options] [sound-name]
93
+
94
+ Options:
95
+ --help, -h Show help
96
+ --list, -l List available sounds
97
+ --config, -c Print config file path
98
+ --init Create default config file
99
+ --version, -v Show version
100
+ ```
101
+
102
+ ## How It Works
103
+
104
+ - **Zero runtime dependencies** — all sound generation is pure Node.js
105
+ - Built-in sounds are generated as WAV audio in-memory (sine waves with envelope)
106
+ - Playback uses native system players: PowerShell SoundPlayer (Windows), afplay (macOS), paplay/aplay (Linux)
107
+ - Falls back to `[System.Console]::Beep()` or terminal bell if WAV playback fails
108
+ - Temp files are cleaned up immediately after playback
109
+
110
+ ## License
111
+
112
+ MIT
package/dist/beep.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { Pattern, Tone } from './types';
2
+ export declare const PATTERNS: Pattern[];
3
+ export declare function getPatternByName(name: string): Pattern | undefined;
4
+ /**
5
+ * Build a complete 16-bit PCM mono WAV buffer from a list of tones.
6
+ * Returns null if the pattern name is not found.
7
+ */
8
+ export declare function buildPatternBuffer(patternName: string, volume?: number): Buffer | null;
9
+ /**
10
+ * Generate a WAV file buffer from an array of Tone objects.
11
+ */
12
+ export declare function generateWav(tones: Tone[], sampleRate: number, volume: number): Buffer;
package/dist/beep.js ADDED
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PATTERNS = void 0;
4
+ exports.getPatternByName = getPatternByName;
5
+ exports.buildPatternBuffer = buildPatternBuffer;
6
+ exports.generateWav = generateWav;
7
+ // ── WAV constants ──────────────────────────────────────────────────────────
8
+ const SAMPLE_RATE = 44100;
9
+ const BITS_PER_SAMPLE = 16;
10
+ const NUM_CHANNELS = 1;
11
+ const BYTES_PER_SAMPLE = BITS_PER_SAMPLE / 8; // 2
12
+ // ── Built-in sound patterns ────────────────────────────────────────────────
13
+ exports.PATTERNS = [
14
+ {
15
+ name: 'short-beep',
16
+ description: 'A single short beep — quick task-complete acknowledgment',
17
+ tones: [{ frequency: 880, duration: 0.15 }],
18
+ },
19
+ {
20
+ name: 'double-beep',
21
+ description: 'Two quick beeps — "something just finished"',
22
+ tones: [
23
+ { frequency: 660, duration: 0.1, gap: 0.08 },
24
+ { frequency: 880, duration: 0.15 },
25
+ ],
26
+ },
27
+ {
28
+ name: 'ascending',
29
+ description: 'Three rising tones — positive progression',
30
+ tones: [
31
+ { frequency: 400, duration: 0.15, gap: 0.05 },
32
+ { frequency: 600, duration: 0.15, gap: 0.05 },
33
+ { frequency: 800, duration: 0.2 },
34
+ ],
35
+ },
36
+ {
37
+ name: 'descending',
38
+ description: 'Three falling tones — task complete, winding down',
39
+ tones: [
40
+ { frequency: 800, duration: 0.15, gap: 0.05 },
41
+ { frequency: 600, duration: 0.15, gap: 0.05 },
42
+ { frequency: 400, duration: 0.2 },
43
+ ],
44
+ },
45
+ {
46
+ name: 'victory',
47
+ description: 'Victory jingle — C5-E5-G5-C6 arpeggio',
48
+ tones: [
49
+ { frequency: 523.25, duration: 0.12, gap: 0.06 },
50
+ { frequency: 659.25, duration: 0.12, gap: 0.06 },
51
+ { frequency: 783.99, duration: 0.12, gap: 0.06 },
52
+ { frequency: 1046.5, duration: 0.3 },
53
+ ],
54
+ },
55
+ ];
56
+ // ── Pattern lookup ─────────────────────────────────────────────────────────
57
+ function getPatternByName(name) {
58
+ return exports.PATTERNS.find((p) => p.name === name);
59
+ }
60
+ // ── WAV generation ─────────────────────────────────────────────────────────
61
+ /**
62
+ * Build a complete 16-bit PCM mono WAV buffer from a list of tones.
63
+ * Returns null if the pattern name is not found.
64
+ */
65
+ function buildPatternBuffer(patternName, volume = 0.7) {
66
+ const pattern = getPatternByName(patternName);
67
+ if (!pattern)
68
+ return null;
69
+ return generateWav(pattern.tones, SAMPLE_RATE, clampVolume(volume));
70
+ }
71
+ /**
72
+ * Generate a WAV file buffer from an array of Tone objects.
73
+ */
74
+ function generateWav(tones, sampleRate, volume) {
75
+ // Calculate total samples needed
76
+ let totalSamples = 0;
77
+ for (const tone of tones) {
78
+ totalSamples += Math.ceil(tone.duration * sampleRate);
79
+ if (tone.gap && tone.gap > 0) {
80
+ totalSamples += Math.ceil(tone.gap * sampleRate);
81
+ }
82
+ }
83
+ const dataSize = totalSamples * BYTES_PER_SAMPLE;
84
+ const headerSize = 44;
85
+ const buffer = Buffer.alloc(headerSize + dataSize);
86
+ // Write WAV header
87
+ writeWavHeader(buffer, dataSize, sampleRate);
88
+ // Generate audio samples
89
+ const samples = generateSamples(tones, sampleRate, volume, totalSamples);
90
+ // Write 16-bit PCM samples (little-endian)
91
+ let offset = headerSize;
92
+ for (let i = 0; i < totalSamples; i++) {
93
+ // Clamp to 16-bit range
94
+ const sample = Math.max(-32768, Math.min(32767, Math.round(samples[i] * 32767)));
95
+ buffer.writeInt16LE(sample, offset);
96
+ offset += 2;
97
+ }
98
+ return buffer;
99
+ }
100
+ // ── Internal helpers ───────────────────────────────────────────────────────
101
+ function clampVolume(v) {
102
+ return Math.max(0, Math.min(1, v));
103
+ }
104
+ function writeWavHeader(buffer, dataSize, sampleRate) {
105
+ const byteRate = sampleRate * NUM_CHANNELS * BYTES_PER_SAMPLE;
106
+ const blockAlign = NUM_CHANNELS * BYTES_PER_SAMPLE;
107
+ // RIFF header
108
+ buffer.write('RIFF', 0, 'ascii');
109
+ buffer.writeUInt32LE(36 + dataSize, 4); // ChunkSize
110
+ buffer.write('WAVE', 8, 'ascii');
111
+ // fmt sub-chunk
112
+ buffer.write('fmt ', 12, 'ascii');
113
+ buffer.writeUInt32LE(16, 16); // Subchunk1Size (PCM = 16)
114
+ buffer.writeUInt16LE(1, 20); // AudioFormat (PCM = 1)
115
+ buffer.writeUInt16LE(NUM_CHANNELS, 22);
116
+ buffer.writeUInt32LE(sampleRate, 24);
117
+ buffer.writeUInt32LE(byteRate, 28);
118
+ buffer.writeUInt16LE(blockAlign, 32);
119
+ buffer.writeUInt16LE(BITS_PER_SAMPLE, 34);
120
+ // data sub-chunk
121
+ buffer.write('data', 36, 'ascii');
122
+ buffer.writeUInt32LE(dataSize, 40);
123
+ }
124
+ function generateSamples(tones, sampleRate, volume, totalSamples) {
125
+ const samples = new Float32Array(totalSamples);
126
+ let offset = 0;
127
+ for (const tone of tones) {
128
+ const toneSamples = Math.ceil(tone.duration * sampleRate);
129
+ const attackSamples = Math.ceil(0.01 * sampleRate); // 10ms attack
130
+ const releaseSamples = Math.ceil(0.02 * sampleRate); // 20ms release
131
+ for (let i = 0; i < toneSamples; i++) {
132
+ const t = i / sampleRate;
133
+ let sample = Math.sin(2 * Math.PI * tone.frequency * t) * volume;
134
+ // Apply envelope to prevent clicks
135
+ if (i < attackSamples) {
136
+ sample *= i / attackSamples; // Linear fade-in
137
+ }
138
+ else if (i >= toneSamples - releaseSamples) {
139
+ sample *= (toneSamples - i) / releaseSamples; // Linear fade-out
140
+ }
141
+ samples[offset + i] = sample;
142
+ }
143
+ offset += toneSamples;
144
+ // Write silence for gap
145
+ if (tone.gap && tone.gap > 0) {
146
+ const gapSamples = Math.ceil(tone.gap * sampleRate);
147
+ offset += gapSamples; // Float32Array is already zero-filled
148
+ }
149
+ }
150
+ return samples;
151
+ }
152
+ //# sourceMappingURL=beep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beep.js","sourceRoot":"","sources":["../src/beep.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { CliAction } from './types';
2
+ export declare function parseArgs(argv: string[]): CliAction;
3
+ export declare function showHelp(): void;
4
+ export declare function listSounds(): void;
package/dist/cli.js ADDED
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseArgs = parseArgs;
4
+ exports.showHelp = showHelp;
5
+ exports.listSounds = listSounds;
6
+ const beep_1 = require("./beep");
7
+ // ── Public API ─────────────────────────────────────────────────────────────
8
+ function parseArgs(argv) {
9
+ const args = argv.slice(); // copy
10
+ if (args.length === 0) {
11
+ return { action: 'play' }; // Play configured sound
12
+ }
13
+ const first = args[0];
14
+ // Handle flags
15
+ switch (first) {
16
+ case '--help':
17
+ case '-h':
18
+ return { action: 'help' };
19
+ case '--list':
20
+ case '-l':
21
+ return { action: 'list' };
22
+ case '--config':
23
+ case '-c':
24
+ return { action: 'config' };
25
+ case '--init':
26
+ return { action: 'init' };
27
+ case '--version':
28
+ case '-v':
29
+ return { action: 'version' };
30
+ }
31
+ // Anything else is treated as a sound name
32
+ return { action: 'play', soundName: first };
33
+ }
34
+ // ── Display helpers ────────────────────────────────────────────────────────
35
+ function showHelp() {
36
+ console.log(`vibe-notify — Notification sounds for Claude Code
37
+
38
+ Usage:
39
+ vibe-notify [options] [sound-name]
40
+
41
+ Options:
42
+ --help, -h Show this help message
43
+ --list, -l List available built-in sounds
44
+ --config, -c Print the config file path
45
+ --init Create a default config file at ~/.vibe-notify/config.json
46
+ --version, -v Show version number
47
+
48
+ Arguments:
49
+ sound-name Play a specific sound (overrides config file):
50
+ ${beep_1.PATTERNS.map((p) => p.name).join(', ')}
51
+
52
+ If no sound-name is given, plays the sound from the config file.
53
+
54
+ Examples:
55
+ vibe-notify victory Play the victory jingle
56
+ vibe-notify --list See all available sounds
57
+ vibe-notify --init Set up your config file
58
+
59
+ Config file: ~/.vibe-notify/config.json (or .vibe-notify.json in project root)
60
+ Claude Code hook: Add to .claude/settings.json → hooks.Stop
61
+ `);
62
+ }
63
+ function listSounds() {
64
+ console.log('Available built-in sounds:\n');
65
+ for (const pattern of beep_1.PATTERNS) {
66
+ console.log(` ${pattern.name.padEnd(14)} ${pattern.description}`);
67
+ }
68
+ console.log(`\n ${'custom'.padEnd(14)} Play a custom .wav file (set "customFile" in config)`);
69
+ console.log(`\nUse "vibe-notify <name>" to play one, or set "sound" in your config file.`);
70
+ }
71
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;AAKA,8BAiCC;AAID,4BA2BC;AAED,gCAOC;AA7ED,iCAAkC;AAElC,8EAA8E;AAE9E,SAAgB,SAAS,CAAC,IAAc;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO;IAElC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,wBAAwB;IACrD,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAEtB,eAAe;IACf,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,QAAQ,CAAC;QACd,KAAK,IAAI;YACP,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAE5B,KAAK,QAAQ,CAAC;QACd,KAAK,IAAI;YACP,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAE5B,KAAK,UAAU,CAAC;QAChB,KAAK,IAAI;YACP,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;QAE9B,KAAK,QAAQ;YACX,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAE5B,KAAK,WAAW,CAAC;QACjB,KAAK,IAAI;YACP,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACjC,CAAC;IAED,2CAA2C;IAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC9C,CAAC;AAED,8EAA8E;AAE9E,SAAgB,QAAQ;IACtB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;qBAcO,eAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;CAW1D,CAAC,CAAC;AACH,CAAC;AAED,SAAgB,UAAU;IACxB,OAAO,CAAC,GAAG,CAAC,8BAA8B,CAAC,CAAC;IAC5C,KAAK,MAAM,OAAO,IAAI,eAAQ,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,OAAO,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,uDAAuD,CAAC,CAAC;IAC/F,OAAO,CAAC,GAAG,CAAC,6EAA6E,CAAC,CAAC;AAC7F,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { VibeNotifyConfig } from './types';
2
+ export declare const DEFAULT_CONFIG: VibeNotifyConfig;
3
+ export declare function getConfigDir(): string;
4
+ export declare function getConfigPath(): string;
5
+ export declare function getConfigFilePath(): string;
6
+ export declare function loadConfig(): VibeNotifyConfig;
7
+ export declare function saveConfig(config: VibeNotifyConfig): void;
8
+ export declare function initConfig(): string;
package/dist/config.js ADDED
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_CONFIG = void 0;
4
+ exports.getConfigDir = getConfigDir;
5
+ exports.getConfigPath = getConfigPath;
6
+ exports.getConfigFilePath = getConfigFilePath;
7
+ exports.loadConfig = loadConfig;
8
+ exports.saveConfig = saveConfig;
9
+ exports.initConfig = initConfig;
10
+ const fs_1 = require("fs");
11
+ const os_1 = require("os");
12
+ const path_1 = require("path");
13
+ // ── Defaults ───────────────────────────────────────────────────────────────
14
+ exports.DEFAULT_CONFIG = {
15
+ sound: 'short-beep',
16
+ customFile: null,
17
+ volume: 0.7,
18
+ playback: {
19
+ method: 'auto',
20
+ },
21
+ };
22
+ // ── Path resolution ────────────────────────────────────────────────────────
23
+ function getConfigDir() {
24
+ return (0, path_1.join)((0, os_1.homedir)(), '.vibe-notify');
25
+ }
26
+ function getConfigPath() {
27
+ // Project-local config takes priority
28
+ const local = (0, path_1.resolve)('.vibe-notify.json');
29
+ if ((0, fs_1.existsSync)(local)) {
30
+ return local;
31
+ }
32
+ // Fall back to global config
33
+ return (0, path_1.join)(getConfigDir(), 'config.json');
34
+ }
35
+ function getConfigFilePath() {
36
+ return getConfigPath();
37
+ }
38
+ // ── Load / Save ────────────────────────────────────────────────────────────
39
+ function loadConfig() {
40
+ const configPath = getConfigPath();
41
+ if (!(0, fs_1.existsSync)(configPath)) {
42
+ return { ...exports.DEFAULT_CONFIG };
43
+ }
44
+ try {
45
+ const raw = (0, fs_1.readFileSync)(configPath, 'utf-8');
46
+ const parsed = JSON.parse(raw);
47
+ return mergeConfig(exports.DEFAULT_CONFIG, parsed);
48
+ }
49
+ catch (err) {
50
+ console.error(`Warning: Could not read config at ${configPath}. Using defaults.`);
51
+ return { ...exports.DEFAULT_CONFIG };
52
+ }
53
+ }
54
+ function saveConfig(config) {
55
+ const configPath = getConfigPath();
56
+ const dir = getConfigDir();
57
+ if (!(0, fs_1.existsSync)(dir)) {
58
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
59
+ }
60
+ (0, fs_1.writeFileSync)(configPath, JSON.stringify(config, null, 2), 'utf-8');
61
+ }
62
+ function initConfig() {
63
+ const configPath = getConfigPath();
64
+ const dir = getConfigDir();
65
+ if (!(0, fs_1.existsSync)(dir)) {
66
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
67
+ }
68
+ if ((0, fs_1.existsSync)(configPath)) {
69
+ return configPath; // Already exists, don't overwrite
70
+ }
71
+ saveConfig({ ...exports.DEFAULT_CONFIG });
72
+ return configPath;
73
+ }
74
+ // ── Helpers ────────────────────────────────────────────────────────────────
75
+ function mergeConfig(defaults, overrides) {
76
+ return {
77
+ ...defaults,
78
+ ...overrides,
79
+ playback: {
80
+ ...defaults.playback,
81
+ ...(overrides.playback || {}),
82
+ },
83
+ };
84
+ }
85
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAkBA,oCAEC;AAED,sCAQC;AAED,8CAEC;AAID,gCAiBC;AAED,gCASC;AAED,gCAcC;AAlFD,2BAAwE;AACxE,2BAA6B;AAC7B,+BAAqC;AAGrC,8EAA8E;AAEjE,QAAA,cAAc,GAAqB;IAC9C,KAAK,EAAE,YAAY;IACnB,UAAU,EAAE,IAAI;IAChB,MAAM,EAAE,GAAG;IACX,QAAQ,EAAE;QACR,MAAM,EAAE,MAAM;KACf;CACF,CAAC;AAEF,8EAA8E;AAE9E,SAAgB,YAAY;IAC1B,OAAO,IAAA,WAAI,EAAC,IAAA,YAAO,GAAE,EAAE,cAAc,CAAC,CAAC;AACzC,CAAC;AAED,SAAgB,aAAa;IAC3B,sCAAsC;IACtC,MAAM,KAAK,GAAG,IAAA,cAAO,EAAC,mBAAmB,CAAC,CAAC;IAC3C,IAAI,IAAA,eAAU,EAAC,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,6BAA6B;IAC7B,OAAO,IAAA,WAAI,EAAC,YAAY,EAAE,EAAE,aAAa,CAAC,CAAC;AAC7C,CAAC;AAED,SAAgB,iBAAiB;IAC/B,OAAO,aAAa,EAAE,CAAC;AACzB,CAAC;AAED,8EAA8E;AAE9E,SAAgB,UAAU;IACxB,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;IAEnC,IAAI,CAAC,IAAA,eAAU,EAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,GAAG,sBAAc,EAAE,CAAC;IAC/B,CAAC;IAED,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAA,iBAAY,EAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,WAAW,CAAC,sBAAc,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CACX,qCAAqC,UAAU,mBAAmB,CACnE,CAAC;QACF,OAAO,EAAE,GAAG,sBAAc,EAAE,CAAC;IAC/B,CAAC;AACH,CAAC;AAED,SAAgB,UAAU,CAAC,MAAwB;IACjD,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;IACnC,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAE3B,IAAI,CAAC,IAAA,eAAU,EAAC,GAAG,CAAC,EAAE,CAAC;QACrB,IAAA,cAAS,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,IAAA,kBAAa,EAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AACtE,CAAC;AAED,SAAgB,UAAU;IACxB,MAAM,UAAU,GAAG,aAAa,EAAE,CAAC;IACnC,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAE3B,IAAI,CAAC,IAAA,eAAU,EAAC,GAAG,CAAC,EAAE,CAAC;QACrB,IAAA,cAAS,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,IAAI,IAAA,eAAU,EAAC,UAAU,CAAC,EAAE,CAAC;QAC3B,OAAO,UAAU,CAAC,CAAC,kCAAkC;IACvD,CAAC;IAED,UAAU,CAAC,EAAE,GAAG,sBAAc,EAAE,CAAC,CAAC;IAClC,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,8EAA8E;AAE9E,SAAS,WAAW,CAClB,QAA0B,EAC1B,SAAoC;IAEpC,OAAO;QACL,GAAG,QAAQ;QACX,GAAG,SAAS;QACZ,QAAQ,EAAE;YACR,GAAG,QAAQ,CAAC,QAAQ;YACpB,GAAG,CAAC,SAAS,CAAC,QAAQ,IAAI,EAAE,CAAC;SAC9B;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const fs_1 = require("fs");
5
+ const cli_1 = require("./cli");
6
+ const config_1 = require("./config");
7
+ const beep_1 = require("./beep");
8
+ const player_1 = require("./player");
9
+ // Read version from package.json
10
+ let VERSION = '0.1.0';
11
+ try {
12
+ // When running from dist/, package.json is one level up
13
+ const pkg = JSON.parse((0, fs_1.readFileSync)(__dirname + '/../package.json', 'utf-8'));
14
+ VERSION = pkg.version || VERSION;
15
+ }
16
+ catch {
17
+ // Fallback version
18
+ }
19
+ async function main() {
20
+ const cli = (0, cli_1.parseArgs)(process.argv.slice(2));
21
+ // ── Info actions (no sound played) ───────────────────────────────────
22
+ switch (cli.action) {
23
+ case 'help':
24
+ (0, cli_1.showHelp)();
25
+ return;
26
+ case 'list':
27
+ (0, cli_1.listSounds)();
28
+ return;
29
+ case 'config':
30
+ console.log((0, config_1.getConfigFilePath)());
31
+ return;
32
+ case 'init': {
33
+ const path = (0, config_1.initConfig)();
34
+ console.log(`Config created at: ${path}`);
35
+ return;
36
+ }
37
+ case 'version':
38
+ console.log(`vibe-notify v${VERSION}`);
39
+ return;
40
+ case 'play':
41
+ // Continue below
42
+ break;
43
+ }
44
+ // ── Play sound ───────────────────────────────────────────────────────
45
+ const config = (0, config_1.loadConfig)();
46
+ const soundName = cli.soundName || config.sound;
47
+ // Validate sound name
48
+ if (soundName !== 'custom' && !(0, beep_1.getPatternByName)(soundName)) {
49
+ console.error(`Unknown sound: "${soundName}"`);
50
+ console.error(`Use --list to see available sounds.`);
51
+ process.exit(1);
52
+ }
53
+ // ── Custom file playback ─────────────────────────────────────────────
54
+ if (soundName === 'custom') {
55
+ if (!config.customFile) {
56
+ console.error('Sound is set to "custom" but no customFile is configured.');
57
+ console.error(`Set "customFile" in ${(0, config_1.getConfigFilePath)()} to a .wav file path.`);
58
+ process.exit(1);
59
+ }
60
+ if (!(0, fs_1.existsSync)(config.customFile)) {
61
+ console.error(`Custom file not found: ${config.customFile}`);
62
+ process.exit(1);
63
+ }
64
+ try {
65
+ await (0, player_1.playFile)(config.customFile, config.playback.method);
66
+ }
67
+ catch (err) {
68
+ console.error(`Failed to play custom file: ${err.message}`);
69
+ process.exit(1);
70
+ }
71
+ return;
72
+ }
73
+ // ── Built-in pattern playback ────────────────────────────────────────
74
+ const buffer = (0, beep_1.buildPatternBuffer)(soundName, config.volume);
75
+ if (buffer) {
76
+ try {
77
+ await (0, player_1.playBuffer)(buffer, config.playback.method);
78
+ }
79
+ catch (err) {
80
+ // WAV playback failed — try fallback beep
81
+ const pattern = (0, beep_1.getPatternByName)(soundName);
82
+ if (pattern) {
83
+ try {
84
+ await (0, player_1.fallbackBeep)(pattern.tones);
85
+ }
86
+ catch {
87
+ // Both methods failed — non-fatal, just warn
88
+ console.error(`Could not play sound: ${err.message}`);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ else {
94
+ // Buffer generation failed — fallback to system beep
95
+ const pattern = (0, beep_1.getPatternByName)(soundName);
96
+ if (pattern) {
97
+ await (0, player_1.fallbackBeep)(pattern.tones);
98
+ }
99
+ }
100
+ }
101
+ main().catch((err) => {
102
+ console.error('vibe-notify error:', err.message);
103
+ process.exit(1);
104
+ });
105
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { Tone, PlaybackMethod } from './types';
2
+ /**
3
+ * Play a WAV buffer through the system audio output.
4
+ * Writes to a temp file, plays it, then cleans up.
5
+ */
6
+ export declare function playBuffer(buffer: Buffer, method?: PlaybackMethod): Promise<void>;
7
+ /**
8
+ * Play a custom audio file (WAV) directly.
9
+ */
10
+ export declare function playFile(filePath: string, method?: PlaybackMethod): Promise<void>;
11
+ /**
12
+ * Fallback: play tones using system beep (no WAV generation needed).
13
+ * Uses [System.Console]::Beep on Windows, terminal BEL on Unix.
14
+ */
15
+ export declare function fallbackBeep(tones: Tone[]): Promise<void>;
package/dist/player.js ADDED
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.playBuffer = playBuffer;
4
+ exports.playFile = playFile;
5
+ exports.fallbackBeep = fallbackBeep;
6
+ const child_process_1 = require("child_process");
7
+ const fs_1 = require("fs");
8
+ const os_1 = require("os");
9
+ const path_1 = require("path");
10
+ // ── Public API ─────────────────────────────────────────────────────────────
11
+ /**
12
+ * Play a WAV buffer through the system audio output.
13
+ * Writes to a temp file, plays it, then cleans up.
14
+ */
15
+ async function playBuffer(buffer, method = 'auto') {
16
+ const tempFile = (0, path_1.join)((0, os_1.tmpdir)(), `vibe-notify-${Date.now()}.wav`);
17
+ try {
18
+ (0, fs_1.writeFileSync)(tempFile, buffer);
19
+ const platform = process.platform;
20
+ if (method === 'powershell' || (method === 'auto' && platform === 'win32')) {
21
+ await playWithPowerShell(tempFile);
22
+ }
23
+ else if (method === 'afplay' || (method === 'auto' && platform === 'darwin')) {
24
+ await playWithAfplay(tempFile);
25
+ }
26
+ else if (method === 'aplay' || (method === 'auto' && platform === 'linux')) {
27
+ await playWithLinux(tempFile);
28
+ }
29
+ else if (method === 'native') {
30
+ // "native" means try whatever the platform supports but don't use powershell/afplay/aplay
31
+ // On Windows this falls through to the beep fallback
32
+ throw new Error('Native method not supported for WAV playback');
33
+ }
34
+ else {
35
+ // Cross-platform fallback: try PowerShell even on non-Windows (WSL, etc.)
36
+ try {
37
+ await playWithPowerShell(tempFile);
38
+ }
39
+ catch {
40
+ await playWithLinux(tempFile);
41
+ }
42
+ }
43
+ }
44
+ finally {
45
+ try {
46
+ (0, fs_1.unlinkSync)(tempFile);
47
+ }
48
+ catch {
49
+ // Temp file cleanup is best-effort
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Play a custom audio file (WAV) directly.
55
+ */
56
+ async function playFile(filePath, method = 'auto') {
57
+ const platform = process.platform;
58
+ if (method === 'powershell' || (method === 'auto' && platform === 'win32')) {
59
+ await playWithPowerShell(filePath);
60
+ }
61
+ else if (method === 'afplay' || (method === 'auto' && platform === 'darwin')) {
62
+ await playWithAfplay(filePath);
63
+ }
64
+ else if (method === 'aplay' || (method === 'auto' && platform === 'linux')) {
65
+ await playWithLinux(filePath);
66
+ }
67
+ else {
68
+ await playWithPowerShell(filePath);
69
+ }
70
+ }
71
+ /**
72
+ * Fallback: play tones using system beep (no WAV generation needed).
73
+ * Uses [System.Console]::Beep on Windows, terminal BEL on Unix.
74
+ */
75
+ async function fallbackBeep(tones) {
76
+ const platform = process.platform;
77
+ for (const tone of tones) {
78
+ if (platform === 'win32') {
79
+ await execPromise('powershell', [
80
+ '-NoProfile',
81
+ '-NonInteractive',
82
+ '-Command',
83
+ `[System.Console]::Beep(${tone.frequency}, ${Math.round(tone.duration * 1000)})`,
84
+ ]);
85
+ }
86
+ else {
87
+ // Terminal bell character
88
+ process.stderr.write('\x07');
89
+ await delay(tone.duration * 1000);
90
+ }
91
+ if (tone.gap && tone.gap > 0) {
92
+ await delay(tone.gap * 1000);
93
+ }
94
+ }
95
+ }
96
+ // ── Platform-specific players ──────────────────────────────────────────────
97
+ async function playWithPowerShell(filePath) {
98
+ const escaped = filePath.replace(/'/g, "''");
99
+ await execPromise('powershell', [
100
+ '-NoProfile',
101
+ '-NonInteractive',
102
+ '-Command',
103
+ `$p = New-Object System.Media.SoundPlayer('${escaped}'); $p.PlaySync()`,
104
+ ]);
105
+ }
106
+ async function playWithAfplay(filePath) {
107
+ await execPromise('afplay', [filePath]);
108
+ }
109
+ async function playWithLinux(filePath) {
110
+ // Try PulseAudio first, fallback to ALSA
111
+ try {
112
+ await execPromise('paplay', [filePath]);
113
+ }
114
+ catch {
115
+ try {
116
+ await execPromise('aplay', [filePath]);
117
+ }
118
+ catch {
119
+ // Last resort: try ffplay if available
120
+ await execPromise('ffplay', ['-nodisp', '-autoexit', '-loglevel', 'quiet', filePath]);
121
+ }
122
+ }
123
+ }
124
+ // ── Utilities ──────────────────────────────────────────────────────────────
125
+ function execPromise(command, args) {
126
+ return new Promise((resolve, reject) => {
127
+ const proc = (0, child_process_1.spawn)(command, args, {
128
+ stdio: 'ignore',
129
+ windowsHide: true,
130
+ });
131
+ proc.on('close', (code) => {
132
+ if (code === 0) {
133
+ resolve();
134
+ }
135
+ else {
136
+ reject(new Error(`${command} exited with code ${code}`));
137
+ }
138
+ });
139
+ proc.on('error', (err) => {
140
+ reject(err);
141
+ });
142
+ });
143
+ }
144
+ function delay(ms) {
145
+ return new Promise((resolve) => setTimeout(resolve, ms));
146
+ }
147
+ //# sourceMappingURL=player.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player.js","sourceRoot":"","sources":["../src/player.ts"],"names":[],"mappings":";;AAYA,gCAmCC;AAKD,4BAeC;AAMD,oCAqBC;AA9FD,iDAAsC;AACtC,2BAA+C;AAC/C,2BAA4B;AAC5B,+BAA4B;AAG5B,8EAA8E;AAE9E;;;GAGG;AACI,KAAK,UAAU,UAAU,CAC9B,MAAc,EACd,SAAyB,MAAM;IAE/B,MAAM,QAAQ,GAAG,IAAA,WAAI,EAAC,IAAA,WAAM,GAAE,EAAE,eAAe,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEjE,IAAI,CAAC;QACH,IAAA,kBAAa,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAChC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QAElC,IAAI,MAAM,KAAK,YAAY,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;YAC3E,MAAM,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QACrC,CAAC;aAAM,IAAI,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,QAAQ,KAAK,QAAQ,CAAC,EAAE,CAAC;YAC/E,MAAM,cAAc,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;aAAM,IAAI,MAAM,KAAK,OAAO,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;YAC7E,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;QAChC,CAAC;aAAM,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC/B,0FAA0F;YAC1F,qDAAqD;YACrD,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAClE,CAAC;aAAM,CAAC;YACN,0EAA0E;YAC1E,IAAI,CAAC;gBACH,MAAM,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YACrC,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;YAChC,CAAC;QACH,CAAC;IACH,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YACH,IAAA,eAAU,EAAC,QAAQ,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,mCAAmC;QACrC,CAAC;IACH,CAAC;AACH,CAAC;AAED;;GAEG;AACI,KAAK,UAAU,QAAQ,CAC5B,QAAgB,EAChB,SAAyB,MAAM;IAE/B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAElC,IAAI,MAAM,KAAK,YAAY,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;QAC3E,MAAM,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;SAAM,IAAI,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,QAAQ,KAAK,QAAQ,CAAC,EAAE,CAAC;QAC/E,MAAM,cAAc,CAAC,QAAQ,CAAC,CAAC;IACjC,CAAC;SAAM,IAAI,MAAM,KAAK,OAAO,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;QAC7E,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;SAAM,CAAC;QACN,MAAM,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,YAAY,CAAC,KAAa;IAC9C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;YACzB,MAAM,WAAW,CAAC,YAAY,EAAE;gBAC9B,YAAY;gBACZ,iBAAiB;gBACjB,UAAU;gBACV,0BAA0B,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG;aACjF,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,0BAA0B;YAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC7B,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;QACpC,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;AACH,CAAC;AAED,8EAA8E;AAE9E,KAAK,UAAU,kBAAkB,CAAC,QAAgB;IAChD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7C,MAAM,WAAW,CAAC,YAAY,EAAE;QAC9B,YAAY;QACZ,iBAAiB;QACjB,UAAU;QACV,6CAA6C,OAAO,mBAAmB;KACxE,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,QAAgB;IAC5C,MAAM,WAAW,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,QAAgB;IAC3C,yCAAyC;IACzC,IAAI,CAAC;QACH,MAAM,WAAW,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YACH,MAAM,WAAW,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QACzC,CAAC;QAAC,MAAM,CAAC;YACP,uCAAuC;YACvC,MAAM,WAAW,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;AACH,CAAC;AAED,8EAA8E;AAE9E,SAAS,WAAW,CAAC,OAAe,EAAE,IAAc;IAClD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,IAAA,qBAAK,EAAC,OAAO,EAAE,IAAI,EAAE;YAChC,KAAK,EAAE,QAAQ;YACf,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,OAAO,EAAE,CAAC;YACZ,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,OAAO,qBAAqB,IAAI,EAAE,CAAC,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACvB,MAAM,CAAC,GAAG,CAAC,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,45 @@
1
+ /** A single tone in a sound pattern */
2
+ export interface Tone {
3
+ /** Frequency in Hz */
4
+ frequency: number;
5
+ /** Duration in seconds */
6
+ duration: number;
7
+ /** Silence after this tone in seconds (optional) */
8
+ gap?: number;
9
+ }
10
+ /** A named sound pattern composed of tones */
11
+ export interface Pattern {
12
+ name: string;
13
+ description: string;
14
+ tones: Tone[];
15
+ }
16
+ /** Supported playback methods */
17
+ export type PlaybackMethod = 'auto' | 'powershell' | 'afplay' | 'aplay' | 'native';
18
+ /** Config file shape */
19
+ export interface VibeNotifyConfig {
20
+ /** Built-in pattern name, or "custom" for a custom file */
21
+ sound: string;
22
+ /** Absolute path to a .wav file (used when sound === "custom") */
23
+ customFile: string | null;
24
+ /** Volume 0.0–1.0 */
25
+ volume: number;
26
+ /** Playback settings */
27
+ playback: {
28
+ method: PlaybackMethod;
29
+ };
30
+ }
31
+ /** CLI action after parsing args */
32
+ export type CliAction = {
33
+ action: 'help';
34
+ } | {
35
+ action: 'list';
36
+ } | {
37
+ action: 'config';
38
+ } | {
39
+ action: 'init';
40
+ } | {
41
+ action: 'version';
42
+ } | {
43
+ action: 'play';
44
+ soundName?: string;
45
+ };
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "vibe-notify",
3
+ "version": "0.1.0",
4
+ "description": "Notification sounds for Claude Code completions — customizable beeps when your session finishes",
5
+ "keywords": ["claude", "claude-code", "notification", "sound", "beep", "alert"],
6
+ "license": "MIT",
7
+ "author": "guant0u",
8
+ "repository": "github:GuanT0u/vibe-notify",
9
+ "homepage": "https://github.com/GuanT0u/vibe-notify#readme",
10
+ "main": "dist/index.js",
11
+ "bin": {
12
+ "vibe-notify": "dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist/",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "prepublishOnly": "npm run build",
22
+ "lint": "tsc --noEmit"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^18.19.0",
26
+ "typescript": "^5.3.0"
27
+ }
28
+ }