claude-code-notify-lite 1.0.1 → 1.0.3
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 +36 -1
- package/bin/cli.js +79 -2
- package/package.json +2 -1
- package/src/audio.js +30 -12
- package/src/index.js +19 -4
- package/src/installer.js +194 -85
- package/src/logger.js +105 -0
- package/src/notifier.js +24 -1
package/README.md
CHANGED
|
@@ -120,12 +120,35 @@ Claude Code Notify Lite integrates with Claude Code's hook system:
|
|
|
120
120
|
|
|
121
121
|
## Troubleshooting
|
|
122
122
|
|
|
123
|
+
### Debug Logs
|
|
124
|
+
|
|
125
|
+
View debug logs to diagnose issues:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Show recent logs
|
|
129
|
+
ccnotify logs
|
|
130
|
+
|
|
131
|
+
# Show more lines
|
|
132
|
+
ccnotify logs -n 100
|
|
133
|
+
|
|
134
|
+
# Clear logs
|
|
135
|
+
ccnotify logs -c
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Log file locations:
|
|
139
|
+
- **Windows:** `%APPDATA%\claude-code-notify-lite\debug.log`
|
|
140
|
+
- **macOS:** `~/Library/Logs/claude-code-notify-lite/debug.log`
|
|
141
|
+
- **Linux:** `~/.local/state/claude-code-notify-lite/debug.log`
|
|
142
|
+
|
|
123
143
|
### Command 'ccnotify' not found
|
|
124
144
|
|
|
125
145
|
After npm global install, if `ccnotify` is not recognized:
|
|
126
146
|
|
|
127
147
|
```bash
|
|
128
|
-
# Use npx instead
|
|
148
|
+
# Use npx instead (short form)
|
|
149
|
+
npx ccnotify install
|
|
150
|
+
|
|
151
|
+
# Or use full package name
|
|
129
152
|
npx claude-code-notify-lite install
|
|
130
153
|
|
|
131
154
|
# Or find npm global bin location
|
|
@@ -133,6 +156,18 @@ npm root -g
|
|
|
133
156
|
# Then add the parent bin directory to PATH
|
|
134
157
|
```
|
|
135
158
|
|
|
159
|
+
### Hook error: "path not found"
|
|
160
|
+
|
|
161
|
+
If you see garbled text like "ϵͳ找不到指定的路径" in hook errors:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Reinstall to update hook command with absolute paths
|
|
165
|
+
npx ccnotify uninstall
|
|
166
|
+
npx ccnotify install
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The latest version uses absolute paths (node.exe + cli.js) instead of npx, which resolves PATH issues in hook execution environment.
|
|
170
|
+
|
|
136
171
|
### Notification not showing
|
|
137
172
|
|
|
138
173
|
**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/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-notify-lite",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Task completion notifications for Claude Code - Cross-platform, lightweight, and easy to use",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
+
"claude-code-notify-lite": "./bin/cli.js",
|
|
7
8
|
"ccnotify": "./bin/cli.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
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',
|
|
@@ -57,18 +62,25 @@ function playWithPowershell(soundPath) {
|
|
|
57
62
|
|
|
58
63
|
exec(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${psScript}"`,
|
|
59
64
|
{ encoding: 'utf8', windowsHide: true },
|
|
60
|
-
() =>
|
|
65
|
+
(err) => {
|
|
66
|
+
if (err) {
|
|
67
|
+
logger.error('PowerShell audio error', err);
|
|
68
|
+
} else {
|
|
69
|
+
logger.info('PowerShell audio completed');
|
|
70
|
+
}
|
|
71
|
+
resolve();
|
|
72
|
+
}
|
|
61
73
|
);
|
|
62
74
|
});
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
function playWithLinuxPlayer(soundPath) {
|
|
66
78
|
return new Promise((resolve) => {
|
|
79
|
+
logger.info('Playing sound on Linux', { soundPath });
|
|
80
|
+
|
|
67
81
|
const tryPlayers = (players) => {
|
|
68
82
|
if (players.length === 0) {
|
|
69
|
-
|
|
70
|
-
console.warn('No audio player available on Linux');
|
|
71
|
-
}
|
|
83
|
+
logger.warn('No audio player available on Linux');
|
|
72
84
|
resolve();
|
|
73
85
|
return;
|
|
74
86
|
}
|
|
@@ -98,10 +110,14 @@ function playWithLinuxPlayer(soundPath) {
|
|
|
98
110
|
cmd = `${player} "${soundPath}"`;
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
logger.debug('Using player', { player, cmd });
|
|
114
|
+
|
|
101
115
|
exec(cmd, (err) => {
|
|
102
116
|
if (err) {
|
|
117
|
+
logger.warn(`${player} failed, trying next`, err);
|
|
103
118
|
tryPlayers(rest);
|
|
104
119
|
} else {
|
|
120
|
+
logger.info(`${player} completed successfully`);
|
|
105
121
|
resolve();
|
|
106
122
|
}
|
|
107
123
|
});
|
|
@@ -113,24 +129,28 @@ function playWithLinuxPlayer(soundPath) {
|
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
function playSound(soundFile) {
|
|
132
|
+
logger.debug('playSound called', { soundFile });
|
|
133
|
+
|
|
116
134
|
const config = loadConfig();
|
|
117
135
|
|
|
118
136
|
if (!config.sound || !config.sound.enabled) {
|
|
137
|
+
logger.info('Sound disabled in config');
|
|
119
138
|
return Promise.resolve();
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
const soundPath = getSoundPath(soundFile || config.sound.file);
|
|
142
|
+
logger.info('Sound path resolved', { soundPath });
|
|
123
143
|
|
|
124
144
|
if (!fs.existsSync(soundPath)) {
|
|
125
|
-
|
|
126
|
-
console.warn(`Sound file not found: ${soundPath}`);
|
|
127
|
-
}
|
|
145
|
+
logger.error('Sound file not found', { soundPath });
|
|
128
146
|
return Promise.resolve();
|
|
129
147
|
}
|
|
130
148
|
|
|
131
149
|
const platform = os.platform();
|
|
132
150
|
const volume = config.sound.volume || 80;
|
|
133
151
|
|
|
152
|
+
logger.info('Playing sound', { platform, volume });
|
|
153
|
+
|
|
134
154
|
if (platform === 'darwin') {
|
|
135
155
|
return playWithAfplay(soundPath, volume);
|
|
136
156
|
} else if (platform === 'win32') {
|
|
@@ -157,9 +177,7 @@ function listSounds() {
|
|
|
157
177
|
}
|
|
158
178
|
});
|
|
159
179
|
} catch (err) {
|
|
160
|
-
|
|
161
|
-
console.warn('Failed to read sounds directory:', err.message);
|
|
162
|
-
}
|
|
180
|
+
logger.error('Failed to read sounds directory', err);
|
|
163
181
|
}
|
|
164
182
|
}
|
|
165
183
|
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { notify } = require('./notifier');
|
|
2
2
|
const { playSound } = require('./audio');
|
|
3
3
|
const { loadConfig } = require('./config');
|
|
4
|
+
const logger = require('./logger');
|
|
4
5
|
|
|
5
6
|
function formatTime() {
|
|
6
7
|
const now = new Date();
|
|
@@ -9,8 +10,11 @@ function formatTime() {
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
async function run(options = {}) {
|
|
13
|
+
logger.info('Run started', { options, cwd: process.cwd(), claudePwd: process.env.CLAUDE_PWD });
|
|
14
|
+
|
|
12
15
|
try {
|
|
13
16
|
const config = loadConfig();
|
|
17
|
+
logger.debug('Config loaded', config);
|
|
14
18
|
|
|
15
19
|
const workDir = process.env.CLAUDE_PWD || process.cwd();
|
|
16
20
|
const time = formatTime();
|
|
@@ -18,7 +22,9 @@ async function run(options = {}) {
|
|
|
18
22
|
const title = options.title || config.notification.title || 'Claude Code';
|
|
19
23
|
const message = options.message || 'Task completed';
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
logger.info('Sending notification', { title, message, workDir, time });
|
|
26
|
+
|
|
27
|
+
const results = await Promise.allSettled([
|
|
22
28
|
notify({
|
|
23
29
|
title,
|
|
24
30
|
message,
|
|
@@ -27,10 +33,19 @@ async function run(options = {}) {
|
|
|
27
33
|
}),
|
|
28
34
|
playSound()
|
|
29
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');
|
|
30
47
|
} catch (err) {
|
|
31
|
-
|
|
32
|
-
console.error('Run error:', err.message);
|
|
33
|
-
}
|
|
48
|
+
logger.error('Run error', err);
|
|
34
49
|
}
|
|
35
50
|
}
|
|
36
51
|
|
package/src/installer.js
CHANGED
|
@@ -1,40 +1,118 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
3
4
|
const { getClaudeConfigDir, getConfigDir, ensureConfigDir, saveConfig, getDefaultConfig } = require('./config');
|
|
5
|
+
const logger = require('./logger');
|
|
4
6
|
|
|
5
7
|
function getClaudeSettingsPath() {
|
|
6
8
|
return path.join(getClaudeConfigDir(), 'settings.json');
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
function isNpxCache(filePath) {
|
|
12
|
+
const normalizedPath = filePath.toLowerCase().replace(/\\/g, '/');
|
|
13
|
+
return normalizedPath.includes('npm-cache/_npx') ||
|
|
14
|
+
normalizedPath.includes('npm-cache\\_npx') ||
|
|
15
|
+
normalizedPath.includes('.npm/_npx');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function copyCliToConfigDir() {
|
|
19
|
+
const configDir = getConfigDir();
|
|
20
|
+
|
|
21
|
+
ensureConfigDir();
|
|
22
|
+
|
|
23
|
+
const srcDir = path.join(configDir, 'src');
|
|
24
|
+
if (!fs.existsSync(srcDir)) {
|
|
25
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const filesToCopy = [
|
|
29
|
+
{ src: path.resolve(__dirname, 'index.js'), dest: path.join(srcDir, 'index.js') },
|
|
30
|
+
{ src: path.resolve(__dirname, 'notifier.js'), dest: path.join(srcDir, 'notifier.js') },
|
|
31
|
+
{ src: path.resolve(__dirname, 'audio.js'), dest: path.join(srcDir, 'audio.js') },
|
|
32
|
+
{ src: path.resolve(__dirname, 'config.js'), dest: path.join(srcDir, 'config.js') },
|
|
33
|
+
{ src: path.resolve(__dirname, 'logger.js'), dest: path.join(srcDir, 'logger.js') }
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
for (const file of filesToCopy) {
|
|
37
|
+
if (fs.existsSync(file.src)) {
|
|
38
|
+
fs.copyFileSync(file.src, file.dest);
|
|
39
|
+
logger.debug('Copied file', { src: file.src, dest: file.dest });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
14
42
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
const assetsDir = path.resolve(__dirname, '..', 'assets');
|
|
44
|
+
const targetAssetsDir = path.join(configDir, 'assets');
|
|
45
|
+
if (!fs.existsSync(targetAssetsDir)) {
|
|
46
|
+
fs.mkdirSync(targetAssetsDir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const soundsDir = path.join(assetsDir, 'sounds');
|
|
50
|
+
const targetSoundsDir = path.join(targetAssetsDir, 'sounds');
|
|
51
|
+
if (fs.existsSync(soundsDir)) {
|
|
52
|
+
if (!fs.existsSync(targetSoundsDir)) {
|
|
53
|
+
fs.mkdirSync(targetSoundsDir, { recursive: true });
|
|
18
54
|
}
|
|
19
|
-
|
|
55
|
+
const sounds = fs.readdirSync(soundsDir);
|
|
56
|
+
for (const sound of sounds) {
|
|
57
|
+
fs.copyFileSync(path.join(soundsDir, sound), path.join(targetSoundsDir, sound));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
20
60
|
|
|
21
|
-
|
|
61
|
+
const runScript = `const path = require('path');
|
|
62
|
+
const srcDir = path.join(__dirname, 'src');
|
|
63
|
+
const { run } = require(path.join(srcDir, 'index.js'));
|
|
64
|
+
run().catch(err => {
|
|
65
|
+
console.error('Notification error:', err.message);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
68
|
+
`;
|
|
69
|
+
const runScriptPath = path.join(configDir, 'run.js');
|
|
70
|
+
fs.writeFileSync(runScriptPath, runScript, 'utf8');
|
|
71
|
+
|
|
72
|
+
logger.info('Copied CLI files to config directory', { configDir });
|
|
73
|
+
return runScriptPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getHookCommand() {
|
|
77
|
+
logger.info('Generating hook command');
|
|
78
|
+
|
|
79
|
+
const nodePath = process.execPath;
|
|
80
|
+
const originalCliPath = path.resolve(__dirname, '..', 'bin', 'cli.js');
|
|
81
|
+
|
|
82
|
+
let cliPath = originalCliPath;
|
|
83
|
+
let useLocalCopy = false;
|
|
84
|
+
let command;
|
|
85
|
+
|
|
86
|
+
if (isNpxCache(originalCliPath)) {
|
|
87
|
+
logger.warn('Running from npx cache, copying files to config directory');
|
|
88
|
+
console.log(' [INFO] Detected npx execution, copying files for persistence...');
|
|
89
|
+
cliPath = copyCliToConfigDir();
|
|
90
|
+
useLocalCopy = true;
|
|
91
|
+
command = `"${nodePath}" "${cliPath}"`;
|
|
92
|
+
} else {
|
|
93
|
+
command = `"${nodePath}" "${cliPath}" run`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
logger.info('Using absolute path command', { command, nodePath, cliPath, useLocalCopy });
|
|
97
|
+
return command;
|
|
22
98
|
}
|
|
23
99
|
|
|
24
100
|
function readClaudeSettings() {
|
|
25
101
|
const settingsPath = getClaudeSettingsPath();
|
|
102
|
+
logger.debug('Reading Claude settings', { settingsPath });
|
|
26
103
|
|
|
27
104
|
if (!fs.existsSync(settingsPath)) {
|
|
105
|
+
logger.debug('Settings file not found, returning empty object');
|
|
28
106
|
return {};
|
|
29
107
|
}
|
|
30
108
|
|
|
31
109
|
try {
|
|
32
110
|
const content = fs.readFileSync(settingsPath, 'utf8');
|
|
33
|
-
|
|
111
|
+
const settings = JSON.parse(content);
|
|
112
|
+
logger.debug('Settings loaded successfully');
|
|
113
|
+
return settings;
|
|
34
114
|
} catch (err) {
|
|
35
|
-
|
|
36
|
-
console.warn('Failed to read Claude settings:', err.message);
|
|
37
|
-
}
|
|
115
|
+
logger.error('Failed to read Claude settings', err);
|
|
38
116
|
return {};
|
|
39
117
|
}
|
|
40
118
|
}
|
|
@@ -43,11 +121,14 @@ function writeClaudeSettings(settings) {
|
|
|
43
121
|
const settingsPath = getClaudeSettingsPath();
|
|
44
122
|
const claudeDir = getClaudeConfigDir();
|
|
45
123
|
|
|
124
|
+
logger.debug('Writing Claude settings', { settingsPath });
|
|
125
|
+
|
|
46
126
|
if (!fs.existsSync(claudeDir)) {
|
|
47
127
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
48
128
|
}
|
|
49
129
|
|
|
50
130
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
131
|
+
logger.info('Settings written successfully');
|
|
51
132
|
}
|
|
52
133
|
|
|
53
134
|
function backupClaudeSettings() {
|
|
@@ -56,6 +137,7 @@ function backupClaudeSettings() {
|
|
|
56
137
|
if (fs.existsSync(settingsPath)) {
|
|
57
138
|
const backupPath = settingsPath + '.backup';
|
|
58
139
|
fs.copyFileSync(settingsPath, backupPath);
|
|
140
|
+
logger.info('Settings backed up', { backupPath });
|
|
59
141
|
return backupPath;
|
|
60
142
|
}
|
|
61
143
|
|
|
@@ -71,101 +153,126 @@ function isOurHook(command) {
|
|
|
71
153
|
|
|
72
154
|
function install(options = {}) {
|
|
73
155
|
console.log('Installing claude-code-notify-lite...\n');
|
|
156
|
+
logger.info('Install started', { options });
|
|
74
157
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
console.log(` [OK] Backed up existing settings to ${backupPath}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const settings = readClaudeSettings();
|
|
90
|
-
|
|
91
|
-
if (!settings.hooks) {
|
|
92
|
-
settings.hooks = {};
|
|
93
|
-
}
|
|
158
|
+
try {
|
|
159
|
+
ensureConfigDir();
|
|
160
|
+
saveConfig(getDefaultConfig());
|
|
161
|
+
console.log(' [OK] Created config file');
|
|
162
|
+
logger.info('Config file created');
|
|
163
|
+
|
|
164
|
+
if (options.skipHooks) {
|
|
165
|
+
console.log(' [SKIP] Hook installation skipped');
|
|
166
|
+
logger.info('Hook installation skipped');
|
|
167
|
+
return { success: true, message: 'Installed without hooks' };
|
|
168
|
+
}
|
|
94
169
|
|
|
95
|
-
|
|
170
|
+
const backupPath = backupClaudeSettings();
|
|
171
|
+
if (backupPath) {
|
|
172
|
+
console.log(` [OK] Backed up existing settings to ${backupPath}`);
|
|
173
|
+
}
|
|
96
174
|
|
|
97
|
-
|
|
98
|
-
hooks: [
|
|
99
|
-
{
|
|
100
|
-
type: 'command',
|
|
101
|
-
command: hookCommand,
|
|
102
|
-
timeout: 10
|
|
103
|
-
}
|
|
104
|
-
]
|
|
105
|
-
};
|
|
175
|
+
const settings = readClaudeSettings();
|
|
106
176
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const exists = settings.hooks.Stop.some(h =>
|
|
111
|
-
h.hooks && h.hooks.some(hh => isOurHook(hh.command))
|
|
112
|
-
);
|
|
177
|
+
if (!settings.hooks) {
|
|
178
|
+
settings.hooks = {};
|
|
179
|
+
}
|
|
113
180
|
|
|
114
|
-
|
|
115
|
-
|
|
181
|
+
const hookCommand = getHookCommand();
|
|
182
|
+
logger.info('Hook command generated', { hookCommand });
|
|
183
|
+
|
|
184
|
+
const stopHook = {
|
|
185
|
+
hooks: [
|
|
186
|
+
{
|
|
187
|
+
type: 'command',
|
|
188
|
+
command: hookCommand,
|
|
189
|
+
timeout: 30
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (!settings.hooks.Stop) {
|
|
195
|
+
settings.hooks.Stop = [stopHook];
|
|
196
|
+
logger.info('Created new Stop hook');
|
|
116
197
|
} else {
|
|
117
|
-
|
|
198
|
+
const existingIndex = settings.hooks.Stop.findIndex(h =>
|
|
199
|
+
h.hooks && h.hooks.some(hh => isOurHook(hh.command))
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (existingIndex === -1) {
|
|
203
|
+
settings.hooks.Stop.push(stopHook);
|
|
204
|
+
logger.info('Added Stop hook');
|
|
205
|
+
} else {
|
|
206
|
+
settings.hooks.Stop[existingIndex] = stopHook;
|
|
207
|
+
console.log(' [OK] Updated existing hook');
|
|
208
|
+
logger.info('Updated existing Stop hook');
|
|
209
|
+
}
|
|
118
210
|
}
|
|
119
|
-
}
|
|
120
211
|
|
|
121
|
-
|
|
122
|
-
|
|
212
|
+
writeClaudeSettings(settings);
|
|
213
|
+
console.log(' [OK] Configured Claude Code hooks');
|
|
123
214
|
|
|
124
|
-
|
|
125
|
-
|
|
215
|
+
console.log('\nInstallation complete!');
|
|
216
|
+
console.log('Run "ccnotify test" or "npx ccnotify test" to verify.\n');
|
|
126
217
|
|
|
127
|
-
|
|
218
|
+
logger.info('Install completed successfully');
|
|
219
|
+
return { success: true };
|
|
220
|
+
} catch (err) {
|
|
221
|
+
logger.error('Install failed', err);
|
|
222
|
+
console.error(` [ERROR] ${err.message}`);
|
|
223
|
+
return { success: false, error: err.message };
|
|
224
|
+
}
|
|
128
225
|
}
|
|
129
226
|
|
|
130
227
|
function uninstall(options = {}) {
|
|
131
228
|
console.log('Uninstalling claude-code-notify-lite...\n');
|
|
229
|
+
logger.info('Uninstall started', { options });
|
|
132
230
|
|
|
133
|
-
|
|
231
|
+
try {
|
|
232
|
+
const settings = readClaudeSettings();
|
|
233
|
+
|
|
234
|
+
if (settings.hooks && settings.hooks.Stop) {
|
|
235
|
+
settings.hooks.Stop = settings.hooks.Stop.filter(h => {
|
|
236
|
+
if (h.hooks) {
|
|
237
|
+
h.hooks = h.hooks.filter(hh => !isOurHook(hh.command));
|
|
238
|
+
return h.hooks.length > 0;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (settings.hooks.Stop.length === 0) {
|
|
244
|
+
delete settings.hooks.Stop;
|
|
245
|
+
}
|
|
134
246
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (h.hooks) {
|
|
138
|
-
h.hooks = h.hooks.filter(hh => !isOurHook(hh.command));
|
|
139
|
-
return h.hooks.length > 0;
|
|
247
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
248
|
+
delete settings.hooks;
|
|
140
249
|
}
|
|
141
|
-
return true;
|
|
142
|
-
});
|
|
143
250
|
|
|
144
|
-
|
|
145
|
-
|
|
251
|
+
writeClaudeSettings(settings);
|
|
252
|
+
console.log(' [OK] Removed Claude Code hooks');
|
|
253
|
+
logger.info('Hooks removed');
|
|
146
254
|
}
|
|
147
255
|
|
|
148
|
-
if (
|
|
149
|
-
|
|
256
|
+
if (!options.keepConfig) {
|
|
257
|
+
const configDir = getConfigDir();
|
|
258
|
+
if (fs.existsSync(configDir)) {
|
|
259
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
260
|
+
console.log(' [OK] Removed config directory');
|
|
261
|
+
logger.info('Config directory removed', { configDir });
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
console.log(' [SKIP] Kept config directory');
|
|
150
265
|
}
|
|
151
266
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
267
|
+
console.log('\nUninstallation complete!\n');
|
|
268
|
+
logger.info('Uninstall completed');
|
|
155
269
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
} else {
|
|
163
|
-
console.log(' [SKIP] Kept config directory');
|
|
270
|
+
return { success: true };
|
|
271
|
+
} catch (err) {
|
|
272
|
+
logger.error('Uninstall failed', err);
|
|
273
|
+
console.error(` [ERROR] ${err.message}`);
|
|
274
|
+
return { success: false, error: err.message };
|
|
164
275
|
}
|
|
165
|
-
|
|
166
|
-
console.log('\nUninstallation complete!\n');
|
|
167
|
-
|
|
168
|
-
return { success: true };
|
|
169
276
|
}
|
|
170
277
|
|
|
171
278
|
function checkInstallation() {
|
|
@@ -180,6 +287,8 @@ function checkInstallation() {
|
|
|
180
287
|
const configDir = getConfigDir();
|
|
181
288
|
const hasConfig = fs.existsSync(path.join(configDir, 'config.json'));
|
|
182
289
|
|
|
290
|
+
logger.debug('Installation check', { hasHook, hasConfig });
|
|
291
|
+
|
|
183
292
|
return {
|
|
184
293
|
installed: hasHook && hasConfig,
|
|
185
294
|
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
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { exec } = require('child_process');
|
|
6
6
|
const { loadConfig } = require('./config');
|
|
7
|
+
const logger = require('./logger');
|
|
7
8
|
|
|
8
9
|
function getIconPath() {
|
|
9
10
|
const iconPath = path.join(__dirname, '..', 'assets', 'icon.png');
|
|
@@ -12,6 +13,8 @@ function getIconPath() {
|
|
|
12
13
|
|
|
13
14
|
function notifyWindows(title, message) {
|
|
14
15
|
return new Promise((resolve) => {
|
|
16
|
+
logger.info('Sending Windows notification', { title, messageLength: message.length });
|
|
17
|
+
|
|
15
18
|
const escapedTitle = title.replace(/"/g, '\\"');
|
|
16
19
|
const escapedMessage = message.replace(/"/g, '\\"').replace(/\n/g, '`n');
|
|
17
20
|
|
|
@@ -38,6 +41,7 @@ function notifyWindows(title, message) {
|
|
|
38
41
|
{ encoding: 'utf8', windowsHide: true },
|
|
39
42
|
(err) => {
|
|
40
43
|
if (err) {
|
|
44
|
+
logger.warn('Windows Toast failed, falling back to node-notifier', err);
|
|
41
45
|
notifier.notify({
|
|
42
46
|
title: title,
|
|
43
47
|
message: message,
|
|
@@ -45,8 +49,16 @@ function notifyWindows(title, message) {
|
|
|
45
49
|
wait: false,
|
|
46
50
|
timeout: 5,
|
|
47
51
|
appID: 'Claude Code'
|
|
48
|
-
}, () =>
|
|
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
|
+
});
|
|
49
60
|
} else {
|
|
61
|
+
logger.info('Windows Toast notification sent successfully');
|
|
50
62
|
resolve();
|
|
51
63
|
}
|
|
52
64
|
}
|
|
@@ -55,9 +67,12 @@ function notifyWindows(title, message) {
|
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
function notify(options = {}) {
|
|
70
|
+
logger.debug('notify called', options);
|
|
71
|
+
|
|
58
72
|
const config = loadConfig();
|
|
59
73
|
|
|
60
74
|
if (!config.notification || !config.notification.enabled) {
|
|
75
|
+
logger.info('Notification disabled in config');
|
|
61
76
|
return Promise.resolve();
|
|
62
77
|
}
|
|
63
78
|
|
|
@@ -73,12 +88,14 @@ function notify(options = {}) {
|
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
const platform = os.platform();
|
|
91
|
+
logger.info('Platform detected', { platform });
|
|
76
92
|
|
|
77
93
|
if (platform === 'win32') {
|
|
78
94
|
return notifyWindows(title, message);
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
const iconPath = getIconPath();
|
|
98
|
+
logger.debug('Icon path', { iconPath });
|
|
82
99
|
|
|
83
100
|
const notificationOptions = {
|
|
84
101
|
title: title,
|
|
@@ -101,11 +118,17 @@ function notify(options = {}) {
|
|
|
101
118
|
|
|
102
119
|
return new Promise((resolve) => {
|
|
103
120
|
const timeoutId = setTimeout(() => {
|
|
121
|
+
logger.warn('Notification timed out');
|
|
104
122
|
resolve();
|
|
105
123
|
}, 10000);
|
|
106
124
|
|
|
107
125
|
notifier.notify(notificationOptions, (err, response) => {
|
|
108
126
|
clearTimeout(timeoutId);
|
|
127
|
+
if (err) {
|
|
128
|
+
logger.error('Notification error', err);
|
|
129
|
+
} else {
|
|
130
|
+
logger.info('Notification sent', { response });
|
|
131
|
+
}
|
|
109
132
|
resolve(response);
|
|
110
133
|
});
|
|
111
134
|
});
|