claude-code-notify-lite 1.0.0 → 1.0.2
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/README.md +25 -0
- package/bin/cli.js +79 -2
- package/bin/notify.js +1 -1
- package/package.json +1 -1
- package/src/audio.js +32 -16
- package/src/index.js +26 -5
- package/src/installer.js +113 -90
- package/src/logger.js +105 -0
- package/src/notifier.js +73 -8
package/README.md
CHANGED
|
@@ -24,6 +24,18 @@ npm install -g claude-code-notify-lite
|
|
|
24
24
|
ccnotify install
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
If `ccnotify` command is not found, use npx instead:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx claude-code-notify-lite install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or add npm global bin to your PATH:
|
|
34
|
+
|
|
35
|
+
**Windows:** Add `%APPDATA%\npm` to your PATH environment variable.
|
|
36
|
+
|
|
37
|
+
**macOS/Linux:** Add `$(npm root -g)/../bin` to your PATH.
|
|
38
|
+
|
|
27
39
|
### Using install script
|
|
28
40
|
|
|
29
41
|
**macOS / Linux:**
|
|
@@ -108,6 +120,19 @@ Claude Code Notify Lite integrates with Claude Code's hook system:
|
|
|
108
120
|
|
|
109
121
|
## Troubleshooting
|
|
110
122
|
|
|
123
|
+
### Command 'ccnotify' not found
|
|
124
|
+
|
|
125
|
+
After npm global install, if `ccnotify` is not recognized:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Use npx instead
|
|
129
|
+
npx claude-code-notify-lite install
|
|
130
|
+
|
|
131
|
+
# Or find npm global bin location
|
|
132
|
+
npm root -g
|
|
133
|
+
# Then add the parent bin directory to PATH
|
|
134
|
+
```
|
|
135
|
+
|
|
111
136
|
### Notification not showing
|
|
112
137
|
|
|
113
138
|
**macOS:**
|
package/bin/cli.js
CHANGED
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
|
+
const fs = require('fs');
|
|
5
6
|
const pkg = require('../package.json');
|
|
6
7
|
|
|
8
|
+
function formatTime() {
|
|
9
|
+
const now = new Date();
|
|
10
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
11
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
program
|
|
8
15
|
.name('ccnotify')
|
|
9
16
|
.description('Task completion notifications for Claude Code')
|
|
@@ -45,6 +52,9 @@ program
|
|
|
45
52
|
|
|
46
53
|
const { notify } = require('../src/notifier');
|
|
47
54
|
const { playSound } = require('../src/audio');
|
|
55
|
+
const logger = require('../src/logger');
|
|
56
|
+
|
|
57
|
+
logger.info('Test started');
|
|
48
58
|
|
|
49
59
|
try {
|
|
50
60
|
await Promise.all([
|
|
@@ -52,7 +62,7 @@ program
|
|
|
52
62
|
title: 'Claude Code Notify',
|
|
53
63
|
message: 'Test notification successful!',
|
|
54
64
|
workDir: process.cwd(),
|
|
55
|
-
time:
|
|
65
|
+
time: formatTime()
|
|
56
66
|
}),
|
|
57
67
|
playSound()
|
|
58
68
|
]);
|
|
@@ -60,9 +70,13 @@ program
|
|
|
60
70
|
console.log(chalk.green(' [OK] Notification sent'));
|
|
61
71
|
console.log(chalk.green(' [OK] Sound played'));
|
|
62
72
|
console.log(chalk.green('\nTest completed successfully!\n'));
|
|
73
|
+
logger.info('Test completed successfully');
|
|
63
74
|
} catch (err) {
|
|
64
75
|
console.log(chalk.red(` [ERROR] ${err.message}`));
|
|
76
|
+
logger.error('Test failed', err);
|
|
65
77
|
}
|
|
78
|
+
|
|
79
|
+
console.log(chalk.gray(`Log file: ${logger.getLogPath()}\n`));
|
|
66
80
|
});
|
|
67
81
|
|
|
68
82
|
program
|
|
@@ -70,6 +84,7 @@ program
|
|
|
70
84
|
.description('Check installation status')
|
|
71
85
|
.action(() => {
|
|
72
86
|
const { checkInstallation } = require('../src/installer');
|
|
87
|
+
const logger = require('../src/logger');
|
|
73
88
|
const status = checkInstallation();
|
|
74
89
|
|
|
75
90
|
console.log(chalk.blue('Installation Status:\n'));
|
|
@@ -83,8 +98,10 @@ program
|
|
|
83
98
|
console.log(` Hook configured: ${status.hasHook ? chalk.green('Yes') : chalk.red('No')}`);
|
|
84
99
|
console.log(` Config exists: ${status.hasConfig ? chalk.green('Yes') : chalk.red('No')}`);
|
|
85
100
|
|
|
101
|
+
console.log(chalk.gray(`\n Log file: ${logger.getLogPath()}`));
|
|
102
|
+
|
|
86
103
|
if (!status.installed) {
|
|
87
|
-
console.log(chalk.yellow('\nRun "
|
|
104
|
+
console.log(chalk.yellow('\nRun "npx claude-code-notify-lite install" to complete installation.\n'));
|
|
88
105
|
}
|
|
89
106
|
});
|
|
90
107
|
|
|
@@ -175,4 +192,64 @@ program
|
|
|
175
192
|
console.log('');
|
|
176
193
|
});
|
|
177
194
|
|
|
195
|
+
program
|
|
196
|
+
.command('logs')
|
|
197
|
+
.description('Show debug logs')
|
|
198
|
+
.option('-n, --lines <number>', 'Number of lines to show', '50')
|
|
199
|
+
.option('-f, --follow', 'Follow log file (like tail -f)')
|
|
200
|
+
.option('-c, --clear', 'Clear log file')
|
|
201
|
+
.action((options) => {
|
|
202
|
+
const logger = require('../src/logger');
|
|
203
|
+
const logPath = logger.getLogPath();
|
|
204
|
+
|
|
205
|
+
if (options.clear) {
|
|
206
|
+
try {
|
|
207
|
+
if (fs.existsSync(logPath)) {
|
|
208
|
+
fs.unlinkSync(logPath);
|
|
209
|
+
console.log(chalk.green('Log file cleared.\n'));
|
|
210
|
+
} else {
|
|
211
|
+
console.log(chalk.yellow('Log file does not exist.\n'));
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.log(chalk.red(`Failed to clear log: ${err.message}\n`));
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(chalk.blue(`Log file: ${logPath}\n`));
|
|
220
|
+
|
|
221
|
+
if (!fs.existsSync(logPath)) {
|
|
222
|
+
console.log(chalk.yellow('No logs yet.\n'));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
228
|
+
const lines = content.trim().split('\n');
|
|
229
|
+
const numLines = parseInt(options.lines, 10) || 50;
|
|
230
|
+
const lastLines = lines.slice(-numLines);
|
|
231
|
+
|
|
232
|
+
if (lastLines.length === 0) {
|
|
233
|
+
console.log(chalk.yellow('Log file is empty.\n'));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log(chalk.gray('--- Recent logs ---\n'));
|
|
238
|
+
lastLines.forEach(line => {
|
|
239
|
+
if (line.includes('[ERROR]')) {
|
|
240
|
+
console.log(chalk.red(line));
|
|
241
|
+
} else if (line.includes('[WARN]')) {
|
|
242
|
+
console.log(chalk.yellow(line));
|
|
243
|
+
} else if (line.includes('[INFO]')) {
|
|
244
|
+
console.log(chalk.white(line));
|
|
245
|
+
} else {
|
|
246
|
+
console.log(chalk.gray(line));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
console.log('');
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.log(chalk.red(`Failed to read log: ${err.message}\n`));
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
178
255
|
program.parse();
|
package/bin/notify.js
CHANGED
package/package.json
CHANGED
package/src/audio.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { exec } = require('child_process');
|
|
5
5
|
const { loadConfig, getSoundsDir } = require('./config');
|
|
6
|
+
const logger = require('./logger');
|
|
6
7
|
|
|
7
8
|
const BUILT_IN_SOUNDS = {
|
|
8
9
|
default: 'default.mp3'
|
|
@@ -33,10 +34,13 @@ function getSoundPath(soundName) {
|
|
|
33
34
|
|
|
34
35
|
function playWithAfplay(soundPath, volume) {
|
|
35
36
|
return new Promise((resolve) => {
|
|
37
|
+
logger.info('Playing sound with afplay', { soundPath, volume });
|
|
36
38
|
const volumeArg = volume < 100 ? `-v ${volume / 100}` : '';
|
|
37
39
|
exec(`afplay ${volumeArg} "${soundPath}"`, (err) => {
|
|
38
|
-
if (err
|
|
39
|
-
|
|
40
|
+
if (err) {
|
|
41
|
+
logger.error('afplay error', err);
|
|
42
|
+
} else {
|
|
43
|
+
logger.info('afplay completed');
|
|
40
44
|
}
|
|
41
45
|
resolve();
|
|
42
46
|
});
|
|
@@ -45,6 +49,7 @@ function playWithAfplay(soundPath, volume) {
|
|
|
45
49
|
|
|
46
50
|
function playWithPowershell(soundPath) {
|
|
47
51
|
return new Promise((resolve) => {
|
|
52
|
+
logger.info('Playing sound with PowerShell', { soundPath });
|
|
48
53
|
const escapedPath = soundPath.replace(/\\/g, '\\\\');
|
|
49
54
|
const psScript = [
|
|
50
55
|
'Add-Type -AssemblyName PresentationCore',
|
|
@@ -55,22 +60,27 @@ function playWithPowershell(soundPath) {
|
|
|
55
60
|
'Start-Sleep -Seconds 3'
|
|
56
61
|
].join('; ');
|
|
57
62
|
|
|
58
|
-
exec(`powershell -NoProfile -Command "${psScript}"`,
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
exec(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${psScript}"`,
|
|
64
|
+
{ encoding: 'utf8', windowsHide: true },
|
|
65
|
+
(err) => {
|
|
66
|
+
if (err) {
|
|
67
|
+
logger.error('PowerShell audio error', err);
|
|
68
|
+
} else {
|
|
69
|
+
logger.info('PowerShell audio completed');
|
|
70
|
+
}
|
|
71
|
+
resolve();
|
|
61
72
|
}
|
|
62
|
-
|
|
63
|
-
});
|
|
73
|
+
);
|
|
64
74
|
});
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
function playWithLinuxPlayer(soundPath) {
|
|
68
78
|
return new Promise((resolve) => {
|
|
79
|
+
logger.info('Playing sound on Linux', { soundPath });
|
|
80
|
+
|
|
69
81
|
const tryPlayers = (players) => {
|
|
70
82
|
if (players.length === 0) {
|
|
71
|
-
|
|
72
|
-
console.warn('No audio player available on Linux');
|
|
73
|
-
}
|
|
83
|
+
logger.warn('No audio player available on Linux');
|
|
74
84
|
resolve();
|
|
75
85
|
return;
|
|
76
86
|
}
|
|
@@ -100,10 +110,14 @@ function playWithLinuxPlayer(soundPath) {
|
|
|
100
110
|
cmd = `${player} "${soundPath}"`;
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
logger.debug('Using player', { player, cmd });
|
|
114
|
+
|
|
103
115
|
exec(cmd, (err) => {
|
|
104
116
|
if (err) {
|
|
117
|
+
logger.warn(`${player} failed, trying next`, err);
|
|
105
118
|
tryPlayers(rest);
|
|
106
119
|
} else {
|
|
120
|
+
logger.info(`${player} completed successfully`);
|
|
107
121
|
resolve();
|
|
108
122
|
}
|
|
109
123
|
});
|
|
@@ -115,24 +129,28 @@ function playWithLinuxPlayer(soundPath) {
|
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
function playSound(soundFile) {
|
|
132
|
+
logger.debug('playSound called', { soundFile });
|
|
133
|
+
|
|
118
134
|
const config = loadConfig();
|
|
119
135
|
|
|
120
136
|
if (!config.sound || !config.sound.enabled) {
|
|
137
|
+
logger.info('Sound disabled in config');
|
|
121
138
|
return Promise.resolve();
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
const soundPath = getSoundPath(soundFile || config.sound.file);
|
|
142
|
+
logger.info('Sound path resolved', { soundPath });
|
|
125
143
|
|
|
126
144
|
if (!fs.existsSync(soundPath)) {
|
|
127
|
-
|
|
128
|
-
console.warn(`Sound file not found: ${soundPath}`);
|
|
129
|
-
}
|
|
145
|
+
logger.error('Sound file not found', { soundPath });
|
|
130
146
|
return Promise.resolve();
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
const platform = os.platform();
|
|
134
150
|
const volume = config.sound.volume || 80;
|
|
135
151
|
|
|
152
|
+
logger.info('Playing sound', { platform, volume });
|
|
153
|
+
|
|
136
154
|
if (platform === 'darwin') {
|
|
137
155
|
return playWithAfplay(soundPath, volume);
|
|
138
156
|
} else if (platform === 'win32') {
|
|
@@ -159,9 +177,7 @@ function listSounds() {
|
|
|
159
177
|
}
|
|
160
178
|
});
|
|
161
179
|
} catch (err) {
|
|
162
|
-
|
|
163
|
-
console.warn('Failed to read sounds directory:', err.message);
|
|
164
|
-
}
|
|
180
|
+
logger.error('Failed to read sounds directory', err);
|
|
165
181
|
}
|
|
166
182
|
}
|
|
167
183
|
|
package/src/index.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
const { notify } = require('./notifier');
|
|
2
2
|
const { playSound } = require('./audio');
|
|
3
3
|
const { loadConfig } = require('./config');
|
|
4
|
+
const logger = require('./logger');
|
|
5
|
+
|
|
6
|
+
function formatTime() {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
9
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
10
|
+
}
|
|
4
11
|
|
|
5
12
|
async function run(options = {}) {
|
|
13
|
+
logger.info('Run started', { options, cwd: process.cwd(), claudePwd: process.env.CLAUDE_PWD });
|
|
14
|
+
|
|
6
15
|
try {
|
|
7
16
|
const config = loadConfig();
|
|
17
|
+
logger.debug('Config loaded', config);
|
|
8
18
|
|
|
9
19
|
const workDir = process.env.CLAUDE_PWD || process.cwd();
|
|
10
|
-
const time =
|
|
20
|
+
const time = formatTime();
|
|
11
21
|
|
|
12
22
|
const title = options.title || config.notification.title || 'Claude Code';
|
|
13
23
|
const message = options.message || 'Task completed';
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
logger.info('Sending notification', { title, message, workDir, time });
|
|
26
|
+
|
|
27
|
+
const results = await Promise.allSettled([
|
|
16
28
|
notify({
|
|
17
29
|
title,
|
|
18
30
|
message,
|
|
@@ -21,10 +33,19 @@ async function run(options = {}) {
|
|
|
21
33
|
}),
|
|
22
34
|
playSound()
|
|
23
35
|
]);
|
|
36
|
+
|
|
37
|
+
results.forEach((result, index) => {
|
|
38
|
+
const taskName = index === 0 ? 'notify' : 'playSound';
|
|
39
|
+
if (result.status === 'fulfilled') {
|
|
40
|
+
logger.info(`${taskName} completed successfully`);
|
|
41
|
+
} else {
|
|
42
|
+
logger.error(`${taskName} failed`, result.reason);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
logger.info('Run completed');
|
|
24
47
|
} catch (err) {
|
|
25
|
-
|
|
26
|
-
console.error('Run error:', err.message);
|
|
27
|
-
}
|
|
48
|
+
logger.error('Run error', err);
|
|
28
49
|
}
|
|
29
50
|
}
|
|
30
51
|
|
package/src/installer.js
CHANGED
|
@@ -1,44 +1,36 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { getClaudeConfigDir, getConfigDir, ensureConfigDir, saveConfig, getDefaultConfig } = require('./config');
|
|
4
|
+
const logger = require('./logger');
|
|
4
5
|
|
|
5
6
|
function getClaudeSettingsPath() {
|
|
6
7
|
return path.join(getClaudeConfigDir(), 'settings.json');
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
function getHookCommand() {
|
|
10
|
-
|
|
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
|
-
}
|
|
11
|
+
logger.info('Generating hook command');
|
|
24
12
|
|
|
25
|
-
|
|
13
|
+
const command = 'npx --yes claude-code-notify-lite run';
|
|
14
|
+
logger.info('Using npx command', { command });
|
|
15
|
+
return command;
|
|
26
16
|
}
|
|
27
17
|
|
|
28
18
|
function readClaudeSettings() {
|
|
29
19
|
const settingsPath = getClaudeSettingsPath();
|
|
20
|
+
logger.debug('Reading Claude settings', { settingsPath });
|
|
30
21
|
|
|
31
22
|
if (!fs.existsSync(settingsPath)) {
|
|
23
|
+
logger.debug('Settings file not found, returning empty object');
|
|
32
24
|
return {};
|
|
33
25
|
}
|
|
34
26
|
|
|
35
27
|
try {
|
|
36
28
|
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
37
|
-
|
|
29
|
+
const settings = JSON.parse(content);
|
|
30
|
+
logger.debug('Settings loaded successfully');
|
|
31
|
+
return settings;
|
|
38
32
|
} catch (err) {
|
|
39
|
-
|
|
40
|
-
console.warn('Failed to read Claude settings:', err.message);
|
|
41
|
-
}
|
|
33
|
+
logger.error('Failed to read Claude settings', err);
|
|
42
34
|
return {};
|
|
43
35
|
}
|
|
44
36
|
}
|
|
@@ -47,11 +39,14 @@ function writeClaudeSettings(settings) {
|
|
|
47
39
|
const settingsPath = getClaudeSettingsPath();
|
|
48
40
|
const claudeDir = getClaudeConfigDir();
|
|
49
41
|
|
|
42
|
+
logger.debug('Writing Claude settings', { settingsPath });
|
|
43
|
+
|
|
50
44
|
if (!fs.existsSync(claudeDir)) {
|
|
51
45
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
49
|
+
logger.info('Settings written successfully');
|
|
55
50
|
}
|
|
56
51
|
|
|
57
52
|
function backupClaudeSettings() {
|
|
@@ -60,6 +55,7 @@ function backupClaudeSettings() {
|
|
|
60
55
|
if (fs.existsSync(settingsPath)) {
|
|
61
56
|
const backupPath = settingsPath + '.backup';
|
|
62
57
|
fs.copyFileSync(settingsPath, backupPath);
|
|
58
|
+
logger.info('Settings backed up', { backupPath });
|
|
63
59
|
return backupPath;
|
|
64
60
|
}
|
|
65
61
|
|
|
@@ -75,101 +71,126 @@ function isOurHook(command) {
|
|
|
75
71
|
|
|
76
72
|
function install(options = {}) {
|
|
77
73
|
console.log('Installing claude-code-notify-lite...\n');
|
|
74
|
+
logger.info('Install started', { options });
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
}
|
|
76
|
+
try {
|
|
77
|
+
ensureConfigDir();
|
|
78
|
+
saveConfig(getDefaultConfig());
|
|
79
|
+
console.log(' [OK] Created config file');
|
|
80
|
+
logger.info('Config file created');
|
|
81
|
+
|
|
82
|
+
if (options.skipHooks) {
|
|
83
|
+
console.log(' [SKIP] Hook installation skipped');
|
|
84
|
+
logger.info('Hook installation skipped');
|
|
85
|
+
return { success: true, message: 'Installed without hooks' };
|
|
86
|
+
}
|
|
98
87
|
|
|
99
|
-
|
|
88
|
+
const backupPath = backupClaudeSettings();
|
|
89
|
+
if (backupPath) {
|
|
90
|
+
console.log(` [OK] Backed up existing settings to ${backupPath}`);
|
|
91
|
+
}
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
hooks: [
|
|
103
|
-
{
|
|
104
|
-
type: 'command',
|
|
105
|
-
command: hookCommand,
|
|
106
|
-
timeout: 10
|
|
107
|
-
}
|
|
108
|
-
]
|
|
109
|
-
};
|
|
93
|
+
const settings = readClaudeSettings();
|
|
110
94
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const exists = settings.hooks.Stop.some(h =>
|
|
115
|
-
h.hooks && h.hooks.some(hh => isOurHook(hh.command))
|
|
116
|
-
);
|
|
95
|
+
if (!settings.hooks) {
|
|
96
|
+
settings.hooks = {};
|
|
97
|
+
}
|
|
117
98
|
|
|
118
|
-
|
|
119
|
-
|
|
99
|
+
const hookCommand = getHookCommand();
|
|
100
|
+
logger.info('Hook command generated', { hookCommand });
|
|
101
|
+
|
|
102
|
+
const stopHook = {
|
|
103
|
+
hooks: [
|
|
104
|
+
{
|
|
105
|
+
type: 'command',
|
|
106
|
+
command: hookCommand,
|
|
107
|
+
timeout: 30
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (!settings.hooks.Stop) {
|
|
113
|
+
settings.hooks.Stop = [stopHook];
|
|
114
|
+
logger.info('Created new Stop hook');
|
|
120
115
|
} else {
|
|
121
|
-
|
|
116
|
+
const existingIndex = settings.hooks.Stop.findIndex(h =>
|
|
117
|
+
h.hooks && h.hooks.some(hh => isOurHook(hh.command))
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (existingIndex === -1) {
|
|
121
|
+
settings.hooks.Stop.push(stopHook);
|
|
122
|
+
logger.info('Added Stop hook');
|
|
123
|
+
} else {
|
|
124
|
+
settings.hooks.Stop[existingIndex] = stopHook;
|
|
125
|
+
console.log(' [OK] Updated existing hook');
|
|
126
|
+
logger.info('Updated existing Stop hook');
|
|
127
|
+
}
|
|
122
128
|
}
|
|
123
|
-
}
|
|
124
129
|
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
writeClaudeSettings(settings);
|
|
131
|
+
console.log(' [OK] Configured Claude Code hooks');
|
|
127
132
|
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
console.log('\nInstallation complete!');
|
|
134
|
+
console.log('Run "npx claude-code-notify-lite test" to verify.\n');
|
|
130
135
|
|
|
131
|
-
|
|
136
|
+
logger.info('Install completed successfully');
|
|
137
|
+
return { success: true };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.error('Install failed', err);
|
|
140
|
+
console.error(` [ERROR] ${err.message}`);
|
|
141
|
+
return { success: false, error: err.message };
|
|
142
|
+
}
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
function uninstall(options = {}) {
|
|
135
146
|
console.log('Uninstalling claude-code-notify-lite...\n');
|
|
147
|
+
logger.info('Uninstall started', { options });
|
|
136
148
|
|
|
137
|
-
|
|
149
|
+
try {
|
|
150
|
+
const settings = readClaudeSettings();
|
|
151
|
+
|
|
152
|
+
if (settings.hooks && settings.hooks.Stop) {
|
|
153
|
+
settings.hooks.Stop = settings.hooks.Stop.filter(h => {
|
|
154
|
+
if (h.hooks) {
|
|
155
|
+
h.hooks = h.hooks.filter(hh => !isOurHook(hh.command));
|
|
156
|
+
return h.hooks.length > 0;
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (settings.hooks.Stop.length === 0) {
|
|
162
|
+
delete settings.hooks.Stop;
|
|
163
|
+
}
|
|
138
164
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (h.hooks) {
|
|
142
|
-
h.hooks = h.hooks.filter(hh => !isOurHook(hh.command));
|
|
143
|
-
return h.hooks.length > 0;
|
|
165
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
166
|
+
delete settings.hooks;
|
|
144
167
|
}
|
|
145
|
-
return true;
|
|
146
|
-
});
|
|
147
168
|
|
|
148
|
-
|
|
149
|
-
|
|
169
|
+
writeClaudeSettings(settings);
|
|
170
|
+
console.log(' [OK] Removed Claude Code hooks');
|
|
171
|
+
logger.info('Hooks removed');
|
|
150
172
|
}
|
|
151
173
|
|
|
152
|
-
if (
|
|
153
|
-
|
|
174
|
+
if (!options.keepConfig) {
|
|
175
|
+
const configDir = getConfigDir();
|
|
176
|
+
if (fs.existsSync(configDir)) {
|
|
177
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
178
|
+
console.log(' [OK] Removed config directory');
|
|
179
|
+
logger.info('Config directory removed', { configDir });
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
console.log(' [SKIP] Kept config directory');
|
|
154
183
|
}
|
|
155
184
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
185
|
+
console.log('\nUninstallation complete!\n');
|
|
186
|
+
logger.info('Uninstall completed');
|
|
159
187
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
console.log(' [SKIP] Kept config directory');
|
|
188
|
+
return { success: true };
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logger.error('Uninstall failed', err);
|
|
191
|
+
console.error(` [ERROR] ${err.message}`);
|
|
192
|
+
return { success: false, error: err.message };
|
|
168
193
|
}
|
|
169
|
-
|
|
170
|
-
console.log('\nUninstallation complete!\n');
|
|
171
|
-
|
|
172
|
-
return { success: true };
|
|
173
194
|
}
|
|
174
195
|
|
|
175
196
|
function checkInstallation() {
|
|
@@ -184,6 +205,8 @@ function checkInstallation() {
|
|
|
184
205
|
const configDir = getConfigDir();
|
|
185
206
|
const hasConfig = fs.existsSync(path.join(configDir, 'config.json'));
|
|
186
207
|
|
|
208
|
+
logger.debug('Installation check', { hasHook, hasConfig });
|
|
209
|
+
|
|
187
210
|
return {
|
|
188
211
|
installed: hasHook && hasConfig,
|
|
189
212
|
hasHook,
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const LOG_DIR_NAME = 'claude-code-notify-lite';
|
|
6
|
+
const LOG_FILE_NAME = 'debug.log';
|
|
7
|
+
const MAX_LOG_SIZE = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function getLogDir() {
|
|
10
|
+
const platform = os.platform();
|
|
11
|
+
|
|
12
|
+
if (platform === 'win32') {
|
|
13
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), LOG_DIR_NAME);
|
|
14
|
+
} else if (platform === 'darwin') {
|
|
15
|
+
return path.join(os.homedir(), 'Library', 'Logs', LOG_DIR_NAME);
|
|
16
|
+
} else {
|
|
17
|
+
return path.join(process.env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state'), LOG_DIR_NAME);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getLogPath() {
|
|
22
|
+
return path.join(getLogDir(), LOG_FILE_NAME);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function ensureLogDir() {
|
|
26
|
+
const logDir = getLogDir();
|
|
27
|
+
if (!fs.existsSync(logDir)) {
|
|
28
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
return logDir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rotateLogIfNeeded() {
|
|
34
|
+
const logPath = getLogPath();
|
|
35
|
+
if (fs.existsSync(logPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const stats = fs.statSync(logPath);
|
|
38
|
+
if (stats.size > MAX_LOG_SIZE) {
|
|
39
|
+
const backupPath = logPath + '.old';
|
|
40
|
+
if (fs.existsSync(backupPath)) {
|
|
41
|
+
fs.unlinkSync(backupPath);
|
|
42
|
+
}
|
|
43
|
+
fs.renameSync(logPath, backupPath);
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatTime() {
|
|
50
|
+
const now = new Date();
|
|
51
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
52
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function log(level, message, data = null) {
|
|
56
|
+
try {
|
|
57
|
+
ensureLogDir();
|
|
58
|
+
rotateLogIfNeeded();
|
|
59
|
+
|
|
60
|
+
const logPath = getLogPath();
|
|
61
|
+
let logLine = `[${formatTime()}] [${level.toUpperCase()}] ${message}`;
|
|
62
|
+
|
|
63
|
+
if (data !== null) {
|
|
64
|
+
if (data instanceof Error) {
|
|
65
|
+
logLine += ` | Error: ${data.message}`;
|
|
66
|
+
if (data.stack) {
|
|
67
|
+
logLine += ` | Stack: ${data.stack.replace(/\n/g, ' -> ')}`;
|
|
68
|
+
}
|
|
69
|
+
} else if (typeof data === 'object') {
|
|
70
|
+
logLine += ` | Data: ${JSON.stringify(data)}`;
|
|
71
|
+
} else {
|
|
72
|
+
logLine += ` | ${data}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logLine += '\n';
|
|
77
|
+
|
|
78
|
+
fs.appendFileSync(logPath, logLine, 'utf8');
|
|
79
|
+
} catch (e) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function info(message, data = null) {
|
|
83
|
+
log('info', message, data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function error(message, data = null) {
|
|
87
|
+
log('error', message, data);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function debug(message, data = null) {
|
|
91
|
+
log('debug', message, data);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function warn(message, data = null) {
|
|
95
|
+
log('warn', message, data);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
info,
|
|
100
|
+
error,
|
|
101
|
+
debug,
|
|
102
|
+
warn,
|
|
103
|
+
getLogPath,
|
|
104
|
+
getLogDir
|
|
105
|
+
};
|
package/src/notifier.js
CHANGED
|
@@ -2,17 +2,77 @@ const notifier = require('node-notifier');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const os = require('os');
|
|
5
|
+
const { exec } = require('child_process');
|
|
5
6
|
const { loadConfig } = require('./config');
|
|
7
|
+
const logger = require('./logger');
|
|
6
8
|
|
|
7
9
|
function getIconPath() {
|
|
8
10
|
const iconPath = path.join(__dirname, '..', 'assets', 'icon.png');
|
|
9
11
|
return fs.existsSync(iconPath) ? iconPath : undefined;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
function notifyWindows(title, message) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
logger.info('Sending Windows notification', { title, messageLength: message.length });
|
|
17
|
+
|
|
18
|
+
const escapedTitle = title.replace(/"/g, '\\"');
|
|
19
|
+
const escapedMessage = message.replace(/"/g, '\\"').replace(/\n/g, '`n');
|
|
20
|
+
|
|
21
|
+
const psScript = `
|
|
22
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
23
|
+
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
|
24
|
+
$template = @"
|
|
25
|
+
<toast>
|
|
26
|
+
<visual>
|
|
27
|
+
<binding template="ToastText02">
|
|
28
|
+
<text id="1">${escapedTitle}</text>
|
|
29
|
+
<text id="2">${escapedMessage}</text>
|
|
30
|
+
</binding>
|
|
31
|
+
</visual>
|
|
32
|
+
</toast>
|
|
33
|
+
"@
|
|
34
|
+
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
35
|
+
$xml.LoadXml($template)
|
|
36
|
+
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
|
37
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Claude Code").Show($toast)
|
|
38
|
+
`.trim();
|
|
39
|
+
|
|
40
|
+
exec(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${psScript.replace(/"/g, '\\"')}"`,
|
|
41
|
+
{ encoding: 'utf8', windowsHide: true },
|
|
42
|
+
(err) => {
|
|
43
|
+
if (err) {
|
|
44
|
+
logger.warn('Windows Toast failed, falling back to node-notifier', err);
|
|
45
|
+
notifier.notify({
|
|
46
|
+
title: title,
|
|
47
|
+
message: message,
|
|
48
|
+
sound: false,
|
|
49
|
+
wait: false,
|
|
50
|
+
timeout: 5,
|
|
51
|
+
appID: 'Claude Code'
|
|
52
|
+
}, (notifyErr) => {
|
|
53
|
+
if (notifyErr) {
|
|
54
|
+
logger.error('node-notifier also failed', notifyErr);
|
|
55
|
+
} else {
|
|
56
|
+
logger.info('Fallback notification sent');
|
|
57
|
+
}
|
|
58
|
+
resolve();
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
logger.info('Windows Toast notification sent successfully');
|
|
62
|
+
resolve();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
12
69
|
function notify(options = {}) {
|
|
70
|
+
logger.debug('notify called', options);
|
|
71
|
+
|
|
13
72
|
const config = loadConfig();
|
|
14
73
|
|
|
15
74
|
if (!config.notification || !config.notification.enabled) {
|
|
75
|
+
logger.info('Notification disabled in config');
|
|
16
76
|
return Promise.resolve();
|
|
17
77
|
}
|
|
18
78
|
|
|
@@ -27,8 +87,15 @@ function notify(options = {}) {
|
|
|
27
87
|
message += `\n${options.time}`;
|
|
28
88
|
}
|
|
29
89
|
|
|
30
|
-
const iconPath = getIconPath();
|
|
31
90
|
const platform = os.platform();
|
|
91
|
+
logger.info('Platform detected', { platform });
|
|
92
|
+
|
|
93
|
+
if (platform === 'win32') {
|
|
94
|
+
return notifyWindows(title, message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const iconPath = getIconPath();
|
|
98
|
+
logger.debug('Icon path', { iconPath });
|
|
32
99
|
|
|
33
100
|
const notificationOptions = {
|
|
34
101
|
title: title,
|
|
@@ -47,22 +114,20 @@ function notify(options = {}) {
|
|
|
47
114
|
notificationOptions.contentImage = iconPath;
|
|
48
115
|
}
|
|
49
116
|
notificationOptions.sound = false;
|
|
50
|
-
} else if (platform === 'win32') {
|
|
51
|
-
notificationOptions.appID = 'Claude Code Notify Lite';
|
|
52
117
|
}
|
|
53
118
|
|
|
54
119
|
return new Promise((resolve) => {
|
|
55
120
|
const timeoutId = setTimeout(() => {
|
|
56
|
-
|
|
57
|
-
console.warn('Notification timed out');
|
|
58
|
-
}
|
|
121
|
+
logger.warn('Notification timed out');
|
|
59
122
|
resolve();
|
|
60
123
|
}, 10000);
|
|
61
124
|
|
|
62
125
|
notifier.notify(notificationOptions, (err, response) => {
|
|
63
126
|
clearTimeout(timeoutId);
|
|
64
|
-
if (err
|
|
65
|
-
|
|
127
|
+
if (err) {
|
|
128
|
+
logger.error('Notification error', err);
|
|
129
|
+
} else {
|
|
130
|
+
logger.info('Notification sent', { response });
|
|
66
131
|
}
|
|
67
132
|
resolve(response);
|
|
68
133
|
});
|