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 +21 -0
- package/README.md +151 -0
- package/assets/sounds/default.mp3 +0 -0
- package/bin/cli.js +178 -0
- package/bin/notify.js +4 -0
- package/package.json +47 -0
- package/scripts/install.ps1 +75 -0
- package/scripts/install.sh +87 -0
- package/src/audio.js +171 -0
- package/src/config.js +110 -0
- package/src/index.js +31 -0
- package/src/installer.js +194 -0
- package/src/notifier.js +72 -0
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
|
+

|
|
8
|
+

|
|
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
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 };
|
package/src/installer.js
ADDED
|
@@ -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 };
|
package/src/notifier.js
ADDED
|
@@ -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 };
|