claude-code-notify-lite 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 km
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,151 @@
1
+ # Claude Code Notify Lite
2
+
3
+ Task completion notifications for [Claude Code](https://claude.ai/code) - Cross-platform, lightweight, and easy to use.
4
+
5
+ [English](#features) | [中文](./README_CN.md)
6
+
7
+ ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-blue)
8
+ ![License](https://img.shields.io/badge/license-MIT-green)
9
+
10
+ ## Features
11
+
12
+ - **Cross-platform** - Works on Windows, macOS, and Linux
13
+ - **Lightweight** - Minimal dependencies, fast startup
14
+ - **Easy to use** - One command installation
15
+ - **Customizable** - Choose your notification sound
16
+ - **Non-intrusive** - Integrates seamlessly with Claude Code
17
+
18
+ ## Quick Start
19
+
20
+ ### Using npm (Recommended)
21
+
22
+ ```bash
23
+ npm install -g claude-code-notify-lite
24
+ ccnotify install
25
+ ```
26
+
27
+ ### Using install script
28
+
29
+ **macOS / Linux:**
30
+ ```bash
31
+ curl -fsSL https://raw.githubusercontent.com/waterpen6/claude-code-notify-lite/main/scripts/install.sh | bash
32
+ ```
33
+
34
+ **Windows (PowerShell):**
35
+ ```powershell
36
+ iwr -useb https://raw.githubusercontent.com/waterpen6/claude-code-notify-lite/main/scripts/install.ps1 | iex
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ After installation, notifications will automatically appear when Claude Code completes a task.
42
+
43
+ ### Commands
44
+
45
+ ```bash
46
+ # Test notification
47
+ ccnotify test
48
+
49
+ # Check installation status
50
+ ccnotify status
51
+
52
+ # Configure settings interactively
53
+ ccnotify config
54
+
55
+ # List available sounds
56
+ ccnotify sounds
57
+
58
+ # Uninstall
59
+ ccnotify uninstall
60
+ ```
61
+
62
+ ## Configuration
63
+
64
+ Configuration file location:
65
+ - **Windows:** `%APPDATA%\claude-code-notify-lite\config.json`
66
+ - **macOS:** `~/Library/Application Support/claude-code-notify-lite/config.json`
67
+ - **Linux:** `~/.config/claude-code-notify-lite/config.json`
68
+
69
+ ### Options
70
+
71
+ ```json
72
+ {
73
+ "notification": {
74
+ "enabled": true,
75
+ "title": "Claude Code",
76
+ "showWorkDir": true,
77
+ "showTime": true
78
+ },
79
+ "sound": {
80
+ "enabled": true,
81
+ "file": "default",
82
+ "volume": 80
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Custom Sound
88
+
89
+ You can use a custom sound file:
90
+
91
+ ```json
92
+ {
93
+ "sound": {
94
+ "file": "/path/to/your/sound.mp3"
95
+ }
96
+ }
97
+ ```
98
+
99
+ Supported formats: MP3, WAV, M4A, OGG
100
+
101
+ ## How It Works
102
+
103
+ Claude Code Notify Lite integrates with Claude Code's hook system:
104
+
105
+ 1. When you run `ccnotify install`, it adds a `Stop` hook to your Claude Code settings
106
+ 2. When Claude Code completes a task, it triggers the hook
107
+ 3. The hook sends a system notification and plays a sound
108
+
109
+ ## Troubleshooting
110
+
111
+ ### Notification not showing
112
+
113
+ **macOS:**
114
+ - Go to System Settings > Notifications
115
+ - Find "Terminal" (or your terminal app) and enable notifications
116
+
117
+ **Windows:**
118
+ - Go to Settings > System > Notifications
119
+ - Ensure notifications are enabled
120
+
121
+ ### Sound not playing
122
+
123
+ - Check system volume
124
+ - Verify the sound file exists: `ccnotify sounds`
125
+ - Try a different sound: `ccnotify config`
126
+
127
+ ### Hook not working
128
+
129
+ ```bash
130
+ # Check installation status
131
+ ccnotify status
132
+
133
+ # Reinstall if needed
134
+ ccnotify uninstall
135
+ ccnotify install
136
+ ```
137
+
138
+ ## Uninstall
139
+
140
+ ```bash
141
+ ccnotify uninstall
142
+ npm uninstall -g claude-code-notify-lite
143
+ ```
144
+
145
+ ## Contributing
146
+
147
+ Contributions are welcome! Please feel free to submit a Pull Request.
148
+
149
+ ## License
150
+
151
+ MIT
Binary file
package/bin/cli.js ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const chalk = require('chalk');
5
+ const pkg = require('../package.json');
6
+
7
+ program
8
+ .name('ccnotify')
9
+ .description('Task completion notifications for Claude Code')
10
+ .version(pkg.version);
11
+
12
+ program
13
+ .command('install')
14
+ .description('Install and configure Claude Code hooks')
15
+ .option('--skip-hooks', 'Skip hook installation')
16
+ .action((options) => {
17
+ const { install } = require('../src/installer');
18
+ install(options);
19
+ });
20
+
21
+ program
22
+ .command('uninstall')
23
+ .description('Uninstall and remove hooks')
24
+ .option('--keep-config', 'Keep configuration files')
25
+ .action((options) => {
26
+ const { uninstall } = require('../src/installer');
27
+ uninstall(options);
28
+ });
29
+
30
+ program
31
+ .command('run')
32
+ .description('Send a notification (used by hooks)')
33
+ .option('-t, --title <title>', 'Notification title')
34
+ .option('-m, --message <message>', 'Notification message')
35
+ .action(async (options) => {
36
+ const { run } = require('../src/index');
37
+ await run(options);
38
+ });
39
+
40
+ program
41
+ .command('test')
42
+ .description('Test notification and sound')
43
+ .action(async () => {
44
+ console.log(chalk.blue('Testing notification...\n'));
45
+
46
+ const { notify } = require('../src/notifier');
47
+ const { playSound } = require('../src/audio');
48
+
49
+ try {
50
+ await Promise.all([
51
+ notify({
52
+ title: 'Claude Code Notify',
53
+ message: 'Test notification successful!',
54
+ workDir: process.cwd(),
55
+ time: new Date().toLocaleString()
56
+ }),
57
+ playSound()
58
+ ]);
59
+
60
+ console.log(chalk.green(' [OK] Notification sent'));
61
+ console.log(chalk.green(' [OK] Sound played'));
62
+ console.log(chalk.green('\nTest completed successfully!\n'));
63
+ } catch (err) {
64
+ console.log(chalk.red(` [ERROR] ${err.message}`));
65
+ }
66
+ });
67
+
68
+ program
69
+ .command('status')
70
+ .description('Check installation status')
71
+ .action(() => {
72
+ const { checkInstallation } = require('../src/installer');
73
+ const status = checkInstallation();
74
+
75
+ console.log(chalk.blue('Installation Status:\n'));
76
+
77
+ if (status.installed) {
78
+ console.log(chalk.green(' [OK] claude-code-notify-lite is installed'));
79
+ } else {
80
+ console.log(chalk.yellow(' [!] claude-code-notify-lite is not fully installed'));
81
+ }
82
+
83
+ console.log(` Hook configured: ${status.hasHook ? chalk.green('Yes') : chalk.red('No')}`);
84
+ console.log(` Config exists: ${status.hasConfig ? chalk.green('Yes') : chalk.red('No')}`);
85
+
86
+ if (!status.installed) {
87
+ console.log(chalk.yellow('\nRun "ccnotify install" to complete installation.\n'));
88
+ }
89
+ });
90
+
91
+ program
92
+ .command('config')
93
+ .description('Configure settings interactively')
94
+ .action(async () => {
95
+ const inquirer = require('inquirer');
96
+ const { loadConfig, saveConfig } = require('../src/config');
97
+ const { listSounds, playSound } = require('../src/audio');
98
+
99
+ const config = loadConfig();
100
+ const sounds = listSounds();
101
+
102
+ const answers = await inquirer.prompt([
103
+ {
104
+ type: 'confirm',
105
+ name: 'notificationEnabled',
106
+ message: 'Enable notifications?',
107
+ default: config.notification.enabled
108
+ },
109
+ {
110
+ type: 'confirm',
111
+ name: 'soundEnabled',
112
+ message: 'Enable sound?',
113
+ default: config.sound.enabled
114
+ },
115
+ {
116
+ type: 'list',
117
+ name: 'soundFile',
118
+ message: 'Select notification sound:',
119
+ choices: sounds,
120
+ default: config.sound.file,
121
+ when: (answers) => answers.soundEnabled
122
+ },
123
+ {
124
+ type: 'confirm',
125
+ name: 'showWorkDir',
126
+ message: 'Show working directory in notification?',
127
+ default: config.notification.showWorkDir
128
+ },
129
+ {
130
+ type: 'confirm',
131
+ name: 'showTime',
132
+ message: 'Show timestamp in notification?',
133
+ default: config.notification.showTime
134
+ }
135
+ ]);
136
+
137
+ config.notification.enabled = answers.notificationEnabled;
138
+ config.sound.enabled = answers.soundEnabled;
139
+ if (answers.soundFile) {
140
+ config.sound.file = answers.soundFile;
141
+ }
142
+ config.notification.showWorkDir = answers.showWorkDir;
143
+ config.notification.showTime = answers.showTime;
144
+
145
+ saveConfig(config);
146
+ console.log(chalk.green('\nConfiguration saved!\n'));
147
+
148
+ if (answers.soundEnabled && answers.soundFile) {
149
+ const testSound = await inquirer.prompt([
150
+ {
151
+ type: 'confirm',
152
+ name: 'test',
153
+ message: 'Play selected sound?',
154
+ default: true
155
+ }
156
+ ]);
157
+
158
+ if (testSound.test) {
159
+ await playSound(answers.soundFile);
160
+ }
161
+ }
162
+ });
163
+
164
+ program
165
+ .command('sounds')
166
+ .description('List available sounds')
167
+ .action(() => {
168
+ const { listSounds } = require('../src/audio');
169
+ const sounds = listSounds();
170
+
171
+ console.log(chalk.blue('Available sounds:\n'));
172
+ sounds.forEach(sound => {
173
+ console.log(` - ${sound}`);
174
+ });
175
+ console.log('');
176
+ });
177
+
178
+ program.parse();
package/bin/notify.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { run } = require('../src/index');
4
+ run().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "claude-code-notify-lite",
3
+ "version": "1.0.0",
4
+ "description": "Task completion notifications for Claude Code - Cross-platform, lightweight, and easy to use",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "ccnotify": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node bin/cli.js test"
11
+ },
12
+ "keywords": [
13
+ "claude",
14
+ "claude-code",
15
+ "notification",
16
+ "notify",
17
+ "cli",
18
+ "terminal",
19
+ "productivity",
20
+ "lite"
21
+ ],
22
+ "author": "km",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/waterpen6/claude-code-notify-lite.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/waterpen6/claude-code-notify-lite/issues"
30
+ },
31
+ "homepage": "https://github.com/waterpen6/claude-code-notify-lite#readme",
32
+ "engines": {
33
+ "node": ">=16.0.0"
34
+ },
35
+ "dependencies": {
36
+ "commander": "^11.0.0",
37
+ "node-notifier": "^10.0.1",
38
+ "chalk": "^4.1.2",
39
+ "inquirer": "^8.2.6"
40
+ },
41
+ "files": [
42
+ "bin",
43
+ "src",
44
+ "assets",
45
+ "scripts"
46
+ ]
47
+ }
@@ -0,0 +1,75 @@
1
+ # Claude Code Notify Lite Installer for Windows
2
+ # Run: iwr -useb https://raw.githubusercontent.com/waterpen6/claude-code-notify-lite/main/scripts/install.ps1 | iex
3
+
4
+ $ErrorActionPreference = "Stop"
5
+
6
+ Write-Host ""
7
+ Write-Host " Claude Code Notify Lite Installer" -ForegroundColor Cyan
8
+ Write-Host " ===================================" -ForegroundColor Cyan
9
+ Write-Host ""
10
+
11
+ function Test-NodeInstalled {
12
+ try {
13
+ $nodeVersion = node -v 2>$null
14
+ if ($nodeVersion) {
15
+ $major = [int]($nodeVersion -replace 'v(\d+)\..*', '$1')
16
+ return $major -ge 16
17
+ }
18
+ } catch {}
19
+ return $false
20
+ }
21
+
22
+ function Install-ViaNpm {
23
+ Write-Host " Installing via npm..." -ForegroundColor Yellow
24
+
25
+ npm install -g claude-code-notify-lite
26
+
27
+ Write-Host ""
28
+ Write-Host " Running setup..." -ForegroundColor Yellow
29
+
30
+ ccnotify install
31
+ }
32
+
33
+ function Install-Binary {
34
+ Write-Host " Downloading binary..." -ForegroundColor Yellow
35
+
36
+ $installDir = Join-Path $env:USERPROFILE ".claude-code-notify-lite"
37
+ $binDir = Join-Path $installDir "bin"
38
+
39
+ if (-not (Test-Path $binDir)) {
40
+ New-Item -ItemType Directory -Path $binDir -Force | Out-Null
41
+ }
42
+
43
+ $arch = if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" }
44
+ $binaryName = "ccnotify-win-$arch.exe"
45
+ $downloadUrl = "https://github.com/waterpen6/claude-code-notify-lite/releases/latest/download/$binaryName"
46
+
47
+ $exePath = Join-Path $binDir "ccnotify.exe"
48
+
49
+ Invoke-WebRequest -Uri $downloadUrl -OutFile $exePath -UseBasicParsing
50
+
51
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
52
+ if ($userPath -notlike "*$binDir*") {
53
+ [Environment]::SetEnvironmentVariable("Path", "$binDir;$userPath", "User")
54
+ $env:Path = "$binDir;$env:Path"
55
+ Write-Host " Added to PATH" -ForegroundColor Green
56
+ }
57
+
58
+ Write-Host ""
59
+ Write-Host " Running setup..." -ForegroundColor Yellow
60
+
61
+ & $exePath install
62
+ }
63
+
64
+ if (Test-NodeInstalled) {
65
+ Install-ViaNpm
66
+ } else {
67
+ Write-Host " Node.js 16+ not found, installing binary..." -ForegroundColor Yellow
68
+ Install-Binary
69
+ }
70
+
71
+ Write-Host ""
72
+ Write-Host " Installation complete!" -ForegroundColor Green
73
+ Write-Host ""
74
+ Write-Host " Run 'ccnotify test' to verify." -ForegroundColor Cyan
75
+ Write-Host ""
@@ -0,0 +1,87 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ REPO="waterpen6/claude-code-notify-lite"
6
+ INSTALL_DIR="$HOME/.claude-code-notify-lite"
7
+
8
+ echo ""
9
+ echo " Claude Code Notify Lite Installer"
10
+ echo " ==================================="
11
+ echo ""
12
+
13
+ check_node() {
14
+ if command -v node &> /dev/null; then
15
+ NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
16
+ if [ "$NODE_VERSION" -ge 16 ]; then
17
+ return 0
18
+ fi
19
+ fi
20
+ return 1
21
+ }
22
+
23
+ install_via_npm() {
24
+ echo " Installing via npm..."
25
+ npm install -g claude-code-notify-lite
26
+ echo ""
27
+ echo " Running setup..."
28
+ ccnotify install
29
+ }
30
+
31
+ install_binary() {
32
+ echo " Downloading binary..."
33
+
34
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
35
+ ARCH=$(uname -m)
36
+
37
+ if [ "$ARCH" = "x86_64" ]; then
38
+ ARCH="x64"
39
+ elif [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then
40
+ ARCH="arm64"
41
+ fi
42
+
43
+ BINARY_NAME="ccnotify-${OS}-${ARCH}"
44
+ DOWNLOAD_URL="https://github.com/${REPO}/releases/latest/download/${BINARY_NAME}"
45
+
46
+ mkdir -p "$INSTALL_DIR/bin"
47
+
48
+ curl -fsSL "$DOWNLOAD_URL" -o "$INSTALL_DIR/bin/ccnotify"
49
+ chmod +x "$INSTALL_DIR/bin/ccnotify"
50
+
51
+ SHELL_RC=""
52
+ if [ -f "$HOME/.zshrc" ]; then
53
+ SHELL_RC="$HOME/.zshrc"
54
+ elif [ -f "$HOME/.bashrc" ]; then
55
+ SHELL_RC="$HOME/.bashrc"
56
+ elif [ -f "$HOME/.bash_profile" ]; then
57
+ SHELL_RC="$HOME/.bash_profile"
58
+ fi
59
+
60
+ if [ -n "$SHELL_RC" ]; then
61
+ if ! grep -q "claude-code-notify-lite" "$SHELL_RC"; then
62
+ echo "" >> "$SHELL_RC"
63
+ echo "# Claude Code Notify Lite" >> "$SHELL_RC"
64
+ echo "export PATH=\"\$HOME/.claude-code-notify-lite/bin:\$PATH\"" >> "$SHELL_RC"
65
+ echo " Added to PATH in $SHELL_RC"
66
+ fi
67
+ fi
68
+
69
+ export PATH="$INSTALL_DIR/bin:$PATH"
70
+
71
+ echo ""
72
+ echo " Running setup..."
73
+ "$INSTALL_DIR/bin/ccnotify" install
74
+ }
75
+
76
+ if check_node; then
77
+ install_via_npm
78
+ else
79
+ echo " Node.js 16+ not found, installing binary..."
80
+ install_binary
81
+ fi
82
+
83
+ echo ""
84
+ echo " Installation complete!"
85
+ echo ""
86
+ echo " Run 'ccnotify test' to verify."
87
+ echo ""
package/src/audio.js ADDED
@@ -0,0 +1,171 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const { exec } = require('child_process');
5
+ const { loadConfig, getSoundsDir } = require('./config');
6
+
7
+ const BUILT_IN_SOUNDS = {
8
+ default: 'default.mp3'
9
+ };
10
+
11
+ const SUPPORTED_FORMATS = ['.mp3', '.wav', '.m4a', '.ogg', '.aiff', '.caf'];
12
+
13
+ function getSoundPath(soundName) {
14
+ if (!soundName || soundName === 'default') {
15
+ return path.join(getSoundsDir(), BUILT_IN_SOUNDS.default);
16
+ }
17
+
18
+ if (BUILT_IN_SOUNDS[soundName]) {
19
+ return path.join(getSoundsDir(), BUILT_IN_SOUNDS[soundName]);
20
+ }
21
+
22
+ if (path.isAbsolute(soundName) && fs.existsSync(soundName)) {
23
+ return soundName;
24
+ }
25
+
26
+ const expandedPath = soundName.replace(/^~/, os.homedir());
27
+ if (fs.existsSync(expandedPath)) {
28
+ return expandedPath;
29
+ }
30
+
31
+ return path.join(getSoundsDir(), BUILT_IN_SOUNDS.default);
32
+ }
33
+
34
+ function playWithAfplay(soundPath, volume) {
35
+ return new Promise((resolve) => {
36
+ const volumeArg = volume < 100 ? `-v ${volume / 100}` : '';
37
+ exec(`afplay ${volumeArg} "${soundPath}"`, (err) => {
38
+ if (err && process.env.DEBUG) {
39
+ console.warn('afplay error:', err.message);
40
+ }
41
+ resolve();
42
+ });
43
+ });
44
+ }
45
+
46
+ function playWithPowershell(soundPath) {
47
+ return new Promise((resolve) => {
48
+ const escapedPath = soundPath.replace(/\\/g, '\\\\');
49
+ const psScript = [
50
+ 'Add-Type -AssemblyName PresentationCore',
51
+ '$player = New-Object System.Windows.Media.MediaPlayer',
52
+ `$player.Open([System.Uri]"${escapedPath}")`,
53
+ 'Start-Sleep -Milliseconds 300',
54
+ '$player.Play()',
55
+ 'Start-Sleep -Seconds 3'
56
+ ].join('; ');
57
+
58
+ exec(`powershell -NoProfile -Command "${psScript}"`, (err) => {
59
+ if (err && process.env.DEBUG) {
60
+ console.warn('PowerShell audio error:', err.message);
61
+ }
62
+ resolve();
63
+ });
64
+ });
65
+ }
66
+
67
+ function playWithLinuxPlayer(soundPath) {
68
+ return new Promise((resolve) => {
69
+ const tryPlayers = (players) => {
70
+ if (players.length === 0) {
71
+ if (process.env.DEBUG) {
72
+ console.warn('No audio player available on Linux');
73
+ }
74
+ resolve();
75
+ return;
76
+ }
77
+
78
+ const [player, ...rest] = players;
79
+ exec(`which ${player}`, (whichErr) => {
80
+ if (whichErr) {
81
+ tryPlayers(rest);
82
+ return;
83
+ }
84
+
85
+ let cmd;
86
+ switch (player) {
87
+ case 'paplay':
88
+ cmd = `paplay "${soundPath}"`;
89
+ break;
90
+ case 'aplay':
91
+ cmd = `aplay "${soundPath}"`;
92
+ break;
93
+ case 'mpv':
94
+ cmd = `mpv --no-video --really-quiet "${soundPath}"`;
95
+ break;
96
+ case 'ffplay':
97
+ cmd = `ffplay -nodisp -autoexit -loglevel quiet "${soundPath}"`;
98
+ break;
99
+ default:
100
+ cmd = `${player} "${soundPath}"`;
101
+ }
102
+
103
+ exec(cmd, (err) => {
104
+ if (err) {
105
+ tryPlayers(rest);
106
+ } else {
107
+ resolve();
108
+ }
109
+ });
110
+ });
111
+ };
112
+
113
+ tryPlayers(['paplay', 'aplay', 'mpv', 'ffplay']);
114
+ });
115
+ }
116
+
117
+ function playSound(soundFile) {
118
+ const config = loadConfig();
119
+
120
+ if (!config.sound || !config.sound.enabled) {
121
+ return Promise.resolve();
122
+ }
123
+
124
+ const soundPath = getSoundPath(soundFile || config.sound.file);
125
+
126
+ if (!fs.existsSync(soundPath)) {
127
+ if (process.env.DEBUG) {
128
+ console.warn(`Sound file not found: ${soundPath}`);
129
+ }
130
+ return Promise.resolve();
131
+ }
132
+
133
+ const platform = os.platform();
134
+ const volume = config.sound.volume || 80;
135
+
136
+ if (platform === 'darwin') {
137
+ return playWithAfplay(soundPath, volume);
138
+ } else if (platform === 'win32') {
139
+ return playWithPowershell(soundPath);
140
+ } else {
141
+ return playWithLinuxPlayer(soundPath);
142
+ }
143
+ }
144
+
145
+ function listSounds() {
146
+ const soundsDir = getSoundsDir();
147
+ const sounds = Object.keys(BUILT_IN_SOUNDS);
148
+
149
+ if (fs.existsSync(soundsDir)) {
150
+ try {
151
+ const files = fs.readdirSync(soundsDir);
152
+ files.forEach(file => {
153
+ const ext = path.extname(file).toLowerCase();
154
+ if (SUPPORTED_FORMATS.includes(ext)) {
155
+ const name = path.basename(file, ext);
156
+ if (!sounds.includes(name)) {
157
+ sounds.push(name);
158
+ }
159
+ }
160
+ });
161
+ } catch (err) {
162
+ if (process.env.DEBUG) {
163
+ console.warn('Failed to read sounds directory:', err.message);
164
+ }
165
+ }
166
+ }
167
+
168
+ return sounds;
169
+ }
170
+
171
+ module.exports = { playSound, getSoundPath, listSounds };
package/src/config.js ADDED
@@ -0,0 +1,110 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ const CONFIG_DIR_NAME = 'claude-code-notify-lite';
6
+ const CONFIG_FILE_NAME = 'config.json';
7
+
8
+ function getConfigDir() {
9
+ const platform = os.platform();
10
+
11
+ if (platform === 'win32') {
12
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), CONFIG_DIR_NAME);
13
+ } else if (platform === 'darwin') {
14
+ return path.join(os.homedir(), 'Library', 'Application Support', CONFIG_DIR_NAME);
15
+ } else {
16
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), CONFIG_DIR_NAME);
17
+ }
18
+ }
19
+
20
+ function getConfigPath() {
21
+ return path.join(getConfigDir(), CONFIG_FILE_NAME);
22
+ }
23
+
24
+ function getClaudeConfigDir() {
25
+ return path.join(os.homedir(), '.claude');
26
+ }
27
+
28
+ function getDefaultConfig() {
29
+ return {
30
+ version: '1.0.0',
31
+ notification: {
32
+ enabled: true,
33
+ title: 'Claude Code',
34
+ showWorkDir: true,
35
+ showTime: true
36
+ },
37
+ sound: {
38
+ enabled: true,
39
+ file: 'default',
40
+ volume: 80
41
+ }
42
+ };
43
+ }
44
+
45
+ function deepMerge(target, source) {
46
+ const result = { ...target };
47
+
48
+ for (const key in source) {
49
+ if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) {
50
+ if (target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])) {
51
+ result[key] = deepMerge(target[key], source[key]);
52
+ } else {
53
+ result[key] = { ...source[key] };
54
+ }
55
+ } else {
56
+ result[key] = source[key];
57
+ }
58
+ }
59
+
60
+ return result;
61
+ }
62
+
63
+ function ensureConfigDir() {
64
+ const configDir = getConfigDir();
65
+ if (!fs.existsSync(configDir)) {
66
+ fs.mkdirSync(configDir, { recursive: true });
67
+ }
68
+ return configDir;
69
+ }
70
+
71
+ function loadConfig() {
72
+ const configPath = getConfigPath();
73
+ const defaultConfig = getDefaultConfig();
74
+
75
+ if (!fs.existsSync(configPath)) {
76
+ return defaultConfig;
77
+ }
78
+
79
+ try {
80
+ const content = fs.readFileSync(configPath, 'utf8');
81
+ const userConfig = JSON.parse(content);
82
+ return deepMerge(defaultConfig, userConfig);
83
+ } catch (error) {
84
+ if (process.env.DEBUG) {
85
+ console.error('Failed to load config:', error.message);
86
+ }
87
+ return defaultConfig;
88
+ }
89
+ }
90
+
91
+ function saveConfig(config) {
92
+ ensureConfigDir();
93
+ const configPath = getConfigPath();
94
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
95
+ }
96
+
97
+ function getSoundsDir() {
98
+ return path.join(__dirname, '..', 'assets', 'sounds');
99
+ }
100
+
101
+ module.exports = {
102
+ getConfigDir,
103
+ getConfigPath,
104
+ getClaudeConfigDir,
105
+ getDefaultConfig,
106
+ ensureConfigDir,
107
+ loadConfig,
108
+ saveConfig,
109
+ getSoundsDir
110
+ };
package/src/index.js ADDED
@@ -0,0 +1,31 @@
1
+ const { notify } = require('./notifier');
2
+ const { playSound } = require('./audio');
3
+ const { loadConfig } = require('./config');
4
+
5
+ async function run(options = {}) {
6
+ try {
7
+ const config = loadConfig();
8
+
9
+ const workDir = process.env.CLAUDE_PWD || process.cwd();
10
+ const time = new Date().toLocaleString();
11
+
12
+ const title = options.title || config.notification.title || 'Claude Code';
13
+ const message = options.message || 'Task completed';
14
+
15
+ await Promise.all([
16
+ notify({
17
+ title,
18
+ message,
19
+ workDir: config.notification.showWorkDir ? workDir : null,
20
+ time: config.notification.showTime ? time : null
21
+ }),
22
+ playSound()
23
+ ]);
24
+ } catch (err) {
25
+ if (process.env.DEBUG) {
26
+ console.error('Run error:', err.message);
27
+ }
28
+ }
29
+ }
30
+
31
+ module.exports = { run };
@@ -0,0 +1,194 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { getClaudeConfigDir, getConfigDir, ensureConfigDir, saveConfig, getDefaultConfig } = require('./config');
4
+
5
+ function getClaudeSettingsPath() {
6
+ return path.join(getClaudeConfigDir(), 'settings.json');
7
+ }
8
+
9
+ function getHookCommand() {
10
+ try {
11
+ const { execSync } = require('child_process');
12
+ const npmRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
13
+ const notifyScript = path.join(npmRoot, 'claude-code-notify-lite', 'bin', 'notify.js');
14
+
15
+ if (fs.existsSync(notifyScript)) {
16
+ const normalizedPath = notifyScript.replace(/\\/g, '/');
17
+ return `node "${normalizedPath}"`;
18
+ }
19
+ } catch (e) {
20
+ if (process.env.DEBUG) {
21
+ console.warn('Failed to find global npm path:', e.message);
22
+ }
23
+ }
24
+
25
+ return 'npx claude-code-notify-lite run';
26
+ }
27
+
28
+ function readClaudeSettings() {
29
+ const settingsPath = getClaudeSettingsPath();
30
+
31
+ if (!fs.existsSync(settingsPath)) {
32
+ return {};
33
+ }
34
+
35
+ try {
36
+ const content = fs.readFileSync(settingsPath, 'utf8');
37
+ return JSON.parse(content);
38
+ } catch (err) {
39
+ if (process.env.DEBUG) {
40
+ console.warn('Failed to read Claude settings:', err.message);
41
+ }
42
+ return {};
43
+ }
44
+ }
45
+
46
+ function writeClaudeSettings(settings) {
47
+ const settingsPath = getClaudeSettingsPath();
48
+ const claudeDir = getClaudeConfigDir();
49
+
50
+ if (!fs.existsSync(claudeDir)) {
51
+ fs.mkdirSync(claudeDir, { recursive: true });
52
+ }
53
+
54
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
55
+ }
56
+
57
+ function backupClaudeSettings() {
58
+ const settingsPath = getClaudeSettingsPath();
59
+
60
+ if (fs.existsSync(settingsPath)) {
61
+ const backupPath = settingsPath + '.backup';
62
+ fs.copyFileSync(settingsPath, backupPath);
63
+ return backupPath;
64
+ }
65
+
66
+ return null;
67
+ }
68
+
69
+ function isOurHook(command) {
70
+ if (!command) return false;
71
+ return command.includes('claude-code-notify-lite') ||
72
+ command.includes('ccnotify') ||
73
+ command.includes('notify.js');
74
+ }
75
+
76
+ function install(options = {}) {
77
+ console.log('Installing claude-code-notify-lite...\n');
78
+
79
+ ensureConfigDir();
80
+ saveConfig(getDefaultConfig());
81
+ console.log(' [OK] Created config file');
82
+
83
+ if (options.skipHooks) {
84
+ console.log(' [SKIP] Hook installation skipped');
85
+ return { success: true, message: 'Installed without hooks' };
86
+ }
87
+
88
+ const backupPath = backupClaudeSettings();
89
+ if (backupPath) {
90
+ console.log(` [OK] Backed up existing settings to ${backupPath}`);
91
+ }
92
+
93
+ const settings = readClaudeSettings();
94
+
95
+ if (!settings.hooks) {
96
+ settings.hooks = {};
97
+ }
98
+
99
+ const hookCommand = getHookCommand();
100
+
101
+ const stopHook = {
102
+ hooks: [
103
+ {
104
+ type: 'command',
105
+ command: hookCommand,
106
+ timeout: 10
107
+ }
108
+ ]
109
+ };
110
+
111
+ if (!settings.hooks.Stop) {
112
+ settings.hooks.Stop = [stopHook];
113
+ } else {
114
+ const exists = settings.hooks.Stop.some(h =>
115
+ h.hooks && h.hooks.some(hh => isOurHook(hh.command))
116
+ );
117
+
118
+ if (!exists) {
119
+ settings.hooks.Stop.push(stopHook);
120
+ } else {
121
+ console.log(' [OK] Hook already configured');
122
+ }
123
+ }
124
+
125
+ writeClaudeSettings(settings);
126
+ console.log(' [OK] Configured Claude Code hooks');
127
+
128
+ console.log('\nInstallation complete!');
129
+ console.log('Run "ccnotify test" to verify.\n');
130
+
131
+ return { success: true };
132
+ }
133
+
134
+ function uninstall(options = {}) {
135
+ console.log('Uninstalling claude-code-notify-lite...\n');
136
+
137
+ const settings = readClaudeSettings();
138
+
139
+ if (settings.hooks && settings.hooks.Stop) {
140
+ settings.hooks.Stop = settings.hooks.Stop.filter(h => {
141
+ if (h.hooks) {
142
+ h.hooks = h.hooks.filter(hh => !isOurHook(hh.command));
143
+ return h.hooks.length > 0;
144
+ }
145
+ return true;
146
+ });
147
+
148
+ if (settings.hooks.Stop.length === 0) {
149
+ delete settings.hooks.Stop;
150
+ }
151
+
152
+ if (Object.keys(settings.hooks).length === 0) {
153
+ delete settings.hooks;
154
+ }
155
+
156
+ writeClaudeSettings(settings);
157
+ console.log(' [OK] Removed Claude Code hooks');
158
+ }
159
+
160
+ if (!options.keepConfig) {
161
+ const configDir = getConfigDir();
162
+ if (fs.existsSync(configDir)) {
163
+ fs.rmSync(configDir, { recursive: true, force: true });
164
+ console.log(' [OK] Removed config directory');
165
+ }
166
+ } else {
167
+ console.log(' [SKIP] Kept config directory');
168
+ }
169
+
170
+ console.log('\nUninstallation complete!\n');
171
+
172
+ return { success: true };
173
+ }
174
+
175
+ function checkInstallation() {
176
+ const settings = readClaudeSettings();
177
+
178
+ const hasHook = settings.hooks &&
179
+ settings.hooks.Stop &&
180
+ settings.hooks.Stop.some(h =>
181
+ h.hooks && h.hooks.some(hh => isOurHook(hh.command))
182
+ );
183
+
184
+ const configDir = getConfigDir();
185
+ const hasConfig = fs.existsSync(path.join(configDir, 'config.json'));
186
+
187
+ return {
188
+ installed: hasHook && hasConfig,
189
+ hasHook,
190
+ hasConfig
191
+ };
192
+ }
193
+
194
+ module.exports = { install, uninstall, checkInstallation };
@@ -0,0 +1,72 @@
1
+ const notifier = require('node-notifier');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const { loadConfig } = require('./config');
6
+
7
+ function getIconPath() {
8
+ const iconPath = path.join(__dirname, '..', 'assets', 'icon.png');
9
+ return fs.existsSync(iconPath) ? iconPath : undefined;
10
+ }
11
+
12
+ function notify(options = {}) {
13
+ const config = loadConfig();
14
+
15
+ if (!config.notification || !config.notification.enabled) {
16
+ return Promise.resolve();
17
+ }
18
+
19
+ const title = options.title || config.notification.title || 'Claude Code';
20
+ let message = options.message || 'Task completed';
21
+
22
+ if (config.notification.showWorkDir && options.workDir) {
23
+ message += `\n${options.workDir}`;
24
+ }
25
+
26
+ if (config.notification.showTime && options.time) {
27
+ message += `\n${options.time}`;
28
+ }
29
+
30
+ const iconPath = getIconPath();
31
+ const platform = os.platform();
32
+
33
+ const notificationOptions = {
34
+ title: title,
35
+ message: message,
36
+ sound: false,
37
+ wait: false,
38
+ timeout: 5
39
+ };
40
+
41
+ if (iconPath) {
42
+ notificationOptions.icon = iconPath;
43
+ }
44
+
45
+ if (platform === 'darwin') {
46
+ if (iconPath) {
47
+ notificationOptions.contentImage = iconPath;
48
+ }
49
+ notificationOptions.sound = false;
50
+ } else if (platform === 'win32') {
51
+ notificationOptions.appID = 'Claude Code Notify Lite';
52
+ }
53
+
54
+ return new Promise((resolve) => {
55
+ const timeoutId = setTimeout(() => {
56
+ if (process.env.DEBUG) {
57
+ console.warn('Notification timed out');
58
+ }
59
+ resolve();
60
+ }, 10000);
61
+
62
+ notifier.notify(notificationOptions, (err, response) => {
63
+ clearTimeout(timeoutId);
64
+ if (err && process.env.DEBUG) {
65
+ console.warn('Notification error:', err.message);
66
+ }
67
+ resolve(response);
68
+ });
69
+ });
70
+ }
71
+
72
+ module.exports = { notify };