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 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: new Date().toLocaleString()
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 "ccnotify install" to complete installation.\n'));
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.1",
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 && process.env.DEBUG) {
39
- console.warn('afplay error:', err.message);
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
- () => resolve()
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
- if (process.env.DEBUG) {
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
- if (process.env.DEBUG) {
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
- if (process.env.DEBUG) {
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
- await Promise.all([
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
- if (process.env.DEBUG) {
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 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');
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
- if (fs.existsSync(notifyScript)) {
16
- const normalizedPath = notifyScript.replace(/\\/g, '/');
17
- return `node "${normalizedPath}"`;
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
- } catch (e) {}
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
- return 'node -e "require(\'claude-code-notify-lite\').run()"';
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
- return JSON.parse(content);
111
+ const settings = JSON.parse(content);
112
+ logger.debug('Settings loaded successfully');
113
+ return settings;
34
114
  } catch (err) {
35
- if (process.env.DEBUG) {
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
- ensureConfigDir();
76
- saveConfig(getDefaultConfig());
77
- console.log(' [OK] Created config file');
78
-
79
- if (options.skipHooks) {
80
- console.log(' [SKIP] Hook installation skipped');
81
- return { success: true, message: 'Installed without hooks' };
82
- }
83
-
84
- const backupPath = backupClaudeSettings();
85
- if (backupPath) {
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
- const hookCommand = getHookCommand();
170
+ const backupPath = backupClaudeSettings();
171
+ if (backupPath) {
172
+ console.log(` [OK] Backed up existing settings to ${backupPath}`);
173
+ }
96
174
 
97
- const stopHook = {
98
- hooks: [
99
- {
100
- type: 'command',
101
- command: hookCommand,
102
- timeout: 10
103
- }
104
- ]
105
- };
175
+ const settings = readClaudeSettings();
106
176
 
107
- if (!settings.hooks.Stop) {
108
- settings.hooks.Stop = [stopHook];
109
- } else {
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
- if (!exists) {
115
- settings.hooks.Stop.push(stopHook);
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
- console.log(' [OK] Hook already configured');
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
- writeClaudeSettings(settings);
122
- console.log(' [OK] Configured Claude Code hooks');
212
+ writeClaudeSettings(settings);
213
+ console.log(' [OK] Configured Claude Code hooks');
123
214
 
124
- console.log('\nInstallation complete!');
125
- console.log('Run "ccnotify test" to verify.\n');
215
+ console.log('\nInstallation complete!');
216
+ console.log('Run "ccnotify test" or "npx ccnotify test" to verify.\n');
126
217
 
127
- return { success: true };
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
- const settings = readClaudeSettings();
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
- if (settings.hooks && settings.hooks.Stop) {
136
- settings.hooks.Stop = settings.hooks.Stop.filter(h => {
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
- if (settings.hooks.Stop.length === 0) {
145
- delete settings.hooks.Stop;
251
+ writeClaudeSettings(settings);
252
+ console.log(' [OK] Removed Claude Code hooks');
253
+ logger.info('Hooks removed');
146
254
  }
147
255
 
148
- if (Object.keys(settings.hooks).length === 0) {
149
- delete settings.hooks;
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
- writeClaudeSettings(settings);
153
- console.log(' [OK] Removed Claude Code hooks');
154
- }
267
+ console.log('\nUninstallation complete!\n');
268
+ logger.info('Uninstall completed');
155
269
 
156
- if (!options.keepConfig) {
157
- const configDir = getConfigDir();
158
- if (fs.existsSync(configDir)) {
159
- fs.rmSync(configDir, { recursive: true, force: true });
160
- console.log(' [OK] Removed config directory');
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
- }, () => resolve());
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
  });