claude-notification-plugin 1.0.4 → 1.0.10
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 +40 -6
- package/bin/install.js +16 -3
- package/notifier/notifier.js +104 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@ Notifications for Claude Code task completion. Sends alerts to Telegram and Wind
|
|
|
7
7
|
- Windows desktop notifications (toast)
|
|
8
8
|
- Telegram bot messages
|
|
9
9
|
- Sound alert
|
|
10
|
-
- Voice announcement
|
|
10
|
+
- Voice announcement (TTS)
|
|
11
11
|
- Skips short tasks (< 15s by default)
|
|
12
|
-
-
|
|
12
|
+
- Granular per-channel enable/disable (globally and per-project)
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
@@ -30,8 +30,13 @@ Config file: `~/.claude/notifier.config.json`
|
|
|
30
30
|
```json
|
|
31
31
|
{
|
|
32
32
|
"telegram": {
|
|
33
|
+
"enabled": true,
|
|
33
34
|
"token": "YOUR_BOT_TOKEN",
|
|
34
|
-
"chatId": "YOUR_CHAT_ID"
|
|
35
|
+
"chatId": "YOUR_CHAT_ID",
|
|
36
|
+
"deleteAfterHours": 24
|
|
37
|
+
},
|
|
38
|
+
"windowsNotification": {
|
|
39
|
+
"enabled": true
|
|
35
40
|
},
|
|
36
41
|
"sound": {
|
|
37
42
|
"enabled": true,
|
|
@@ -44,11 +49,40 @@ Config file: `~/.claude/notifier.config.json`
|
|
|
44
49
|
}
|
|
45
50
|
```
|
|
46
51
|
|
|
52
|
+
Each channel has an `enabled` flag (`true`/`false`) for global control.
|
|
53
|
+
|
|
54
|
+
`deleteAfterHours` — auto-delete old Telegram messages after the specified number of hours (default: `24`, set `0` to disable).
|
|
55
|
+
|
|
47
56
|
Environment variables `TELEGRAM_TOKEN` and `TELEGRAM_CHAT_ID` override config file values.
|
|
48
57
|
|
|
49
|
-
###
|
|
58
|
+
### Per-channel environment variables
|
|
59
|
+
|
|
60
|
+
These env vars override the global config per channel (`"1"` = on, `"0"` = off):
|
|
61
|
+
|
|
62
|
+
| Variable | Channel |
|
|
63
|
+
|--------------------------|----------------------------|
|
|
64
|
+
| `CLAUDE_NOTIFY_TELEGRAM` | Telegram messages |
|
|
65
|
+
| `CLAUDE_NOTIFY_WINDOWS` | Windows toast notifications|
|
|
66
|
+
| `CLAUDE_NOTIFY_SOUND` | Sound alert |
|
|
67
|
+
| `CLAUDE_NOTIFY_VOICE` | Voice announcement (TTS) |
|
|
68
|
+
|
|
69
|
+
### Per-project configuration
|
|
70
|
+
|
|
71
|
+
Add to `.claude/settings.local.json` in the project root to control channels per project:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"env": {
|
|
76
|
+
"DISABLE_CLAUDE_NOTIFIER": 0,
|
|
77
|
+
"CLAUDE_NOTIFY_TELEGRAM": 1,
|
|
78
|
+
"CLAUDE_NOTIFY_WINDOWS": 1,
|
|
79
|
+
"CLAUDE_NOTIFY_SOUND": 1,
|
|
80
|
+
"CLAUDE_NOTIFY_VOICE": 1
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
50
84
|
|
|
51
|
-
|
|
85
|
+
To disable all notifications for a project:
|
|
52
86
|
|
|
53
87
|
```json
|
|
54
88
|
{
|
|
@@ -63,7 +97,7 @@ Add to `.claude/settings.local.json` in the project root:
|
|
|
63
97
|
1. Open Telegram, find **@BotFather**
|
|
64
98
|
2. Send `/newbot`, follow prompts, pick a name
|
|
65
99
|
3. Copy the bot token (format: `123456789:ABCdef...`)
|
|
66
|
-
4. Send any message to your new bot
|
|
100
|
+
4. **Send any message to your new bot**
|
|
67
101
|
5. Open `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates`
|
|
68
102
|
6. Find `"chat":{"id":123456789}` in the response — that's your Chat ID
|
|
69
103
|
7. Run `claude-notify-install` and enter the token and chat ID
|
package/bin/install.js
CHANGED
|
@@ -112,9 +112,22 @@ async function main () {
|
|
|
112
112
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
113
113
|
|
|
114
114
|
const config = {
|
|
115
|
-
telegram: {
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
telegram: {
|
|
116
|
+
enabled: true,
|
|
117
|
+
token,
|
|
118
|
+
chatId,
|
|
119
|
+
deleteAfterHours: 24,
|
|
120
|
+
},
|
|
121
|
+
windowsNotification: {
|
|
122
|
+
enabled: true,
|
|
123
|
+
},
|
|
124
|
+
sound: {
|
|
125
|
+
enabled: true,
|
|
126
|
+
file: 'C:/Windows/Media/notify.wav',
|
|
127
|
+
},
|
|
128
|
+
voice: {
|
|
129
|
+
enabled: true,
|
|
130
|
+
},
|
|
118
131
|
minSeconds: 15,
|
|
119
132
|
};
|
|
120
133
|
|
package/notifier/notifier.js
CHANGED
|
@@ -18,9 +18,22 @@ function loadConfig () {
|
|
|
18
18
|
const configPath = path.join(os.homedir(), '.claude', 'notifier.config.json');
|
|
19
19
|
|
|
20
20
|
const config = {
|
|
21
|
-
telegram: {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
telegram: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
token: '',
|
|
24
|
+
chatId: '',
|
|
25
|
+
deleteAfterHours: 24,
|
|
26
|
+
},
|
|
27
|
+
windowsNotification: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
},
|
|
30
|
+
sound: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
file: 'C:/Windows/Media/notify.wav',
|
|
33
|
+
},
|
|
34
|
+
voice: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
},
|
|
24
37
|
minSeconds: 15,
|
|
25
38
|
};
|
|
26
39
|
|
|
@@ -31,6 +44,9 @@ function loadConfig () {
|
|
|
31
44
|
if (user.telegram) {
|
|
32
45
|
config.telegram = { ...config.telegram, ...user.telegram };
|
|
33
46
|
}
|
|
47
|
+
if (user.windowsNotification) {
|
|
48
|
+
config.windowsNotification = { ...config.windowsNotification, ...user.windowsNotification };
|
|
49
|
+
}
|
|
34
50
|
if (user.sound) {
|
|
35
51
|
config.sound = { ...config.sound, ...user.sound };
|
|
36
52
|
}
|
|
@@ -52,6 +68,20 @@ function loadConfig () {
|
|
|
52
68
|
config.telegram.chatId = process.env.TELEGRAM_CHAT_ID;
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
// Per-channel env overrides (0 = off, 1 = on)
|
|
72
|
+
if (process.env.CLAUDE_NOTIFY_TELEGRAM !== undefined) {
|
|
73
|
+
config.telegram.enabled = process.env.CLAUDE_NOTIFY_TELEGRAM === '1';
|
|
74
|
+
}
|
|
75
|
+
if (process.env.CLAUDE_NOTIFY_WINDOWS !== undefined) {
|
|
76
|
+
config.windowsNotification.enabled = process.env.CLAUDE_NOTIFY_WINDOWS === '1';
|
|
77
|
+
}
|
|
78
|
+
if (process.env.CLAUDE_NOTIFY_SOUND !== undefined) {
|
|
79
|
+
config.sound.enabled = process.env.CLAUDE_NOTIFY_SOUND === '1';
|
|
80
|
+
}
|
|
81
|
+
if (process.env.CLAUDE_NOTIFY_VOICE !== undefined) {
|
|
82
|
+
config.voice.enabled = process.env.CLAUDE_NOTIFY_VOICE === '1';
|
|
83
|
+
}
|
|
84
|
+
|
|
55
85
|
return config;
|
|
56
86
|
}
|
|
57
87
|
|
|
@@ -95,33 +125,72 @@ function saveState (state) {
|
|
|
95
125
|
// TELEGRAM
|
|
96
126
|
// ----------------------
|
|
97
127
|
|
|
98
|
-
async function sendTelegram (config,
|
|
99
|
-
if (!config.telegram.token || !config.telegram.chatId) {
|
|
128
|
+
async function sendTelegram (config, state) {
|
|
129
|
+
if (!config.telegram.enabled || !config.telegram.token || !config.telegram.chatId) {
|
|
100
130
|
return;
|
|
101
131
|
}
|
|
102
132
|
|
|
103
|
-
const
|
|
104
|
-
`https://api.telegram.org/bot${config.telegram.token}/sendMessage`;
|
|
133
|
+
const baseUrl = `https://api.telegram.org/bot${config.telegram.token}`;
|
|
105
134
|
|
|
135
|
+
// Send new message and store its id
|
|
106
136
|
try {
|
|
107
|
-
await fetch(
|
|
137
|
+
const res = await fetch(`${baseUrl}/sendMessage`, {
|
|
108
138
|
method: 'POST',
|
|
109
139
|
headers: { 'Content-Type': 'application/json' },
|
|
110
140
|
body: JSON.stringify({
|
|
111
141
|
chat_id: config.telegram.chatId,
|
|
112
|
-
text,
|
|
142
|
+
text: state._telegramText,
|
|
113
143
|
}),
|
|
114
144
|
});
|
|
145
|
+
const data = await res.json();
|
|
146
|
+
if (data.ok && data.result?.message_id) {
|
|
147
|
+
if (!state.sentMessages) {
|
|
148
|
+
state.sentMessages = [];
|
|
149
|
+
}
|
|
150
|
+
state.sentMessages.push({
|
|
151
|
+
id: data.result.message_id,
|
|
152
|
+
ts: Date.now(),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
115
155
|
} catch {
|
|
116
156
|
// silent fail
|
|
117
157
|
}
|
|
158
|
+
|
|
159
|
+
// Delete old messages
|
|
160
|
+
const maxAge = (config.telegram.deleteAfterHours || 24) * 3600_000;
|
|
161
|
+
if (state.sentMessages?.length) {
|
|
162
|
+
const now = Date.now();
|
|
163
|
+
const keep = [];
|
|
164
|
+
for (const msg of state.sentMessages) {
|
|
165
|
+
if (now - msg.ts > maxAge) {
|
|
166
|
+
try {
|
|
167
|
+
await fetch(`${baseUrl}/deleteMessage`, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: { 'Content-Type': 'application/json' },
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
chat_id: config.telegram.chatId,
|
|
172
|
+
message_id: msg.id,
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
// silent fail
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
keep.push(msg);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
state.sentMessages = keep;
|
|
183
|
+
}
|
|
118
184
|
}
|
|
119
185
|
|
|
120
186
|
// ----------------------
|
|
121
187
|
// WINDOWS NOTIFICATION
|
|
122
188
|
// ----------------------
|
|
123
189
|
|
|
124
|
-
function sendWindowsNotification (message) {
|
|
190
|
+
function sendWindowsNotification (config, message) {
|
|
191
|
+
if (!config.windowsNotification.enabled) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
125
194
|
notifier.notify({
|
|
126
195
|
title: 'Claude Code',
|
|
127
196
|
message,
|
|
@@ -145,12 +214,30 @@ function playSound (config) {
|
|
|
145
214
|
}
|
|
146
215
|
}
|
|
147
216
|
|
|
217
|
+
const voicePhrases = {
|
|
218
|
+
en: (d) => `Claude finished coding in ${d} seconds`,
|
|
219
|
+
ru: (d) => `Клод завершил работу за ${d} секунд`,
|
|
220
|
+
de: (d) => `Claude hat die Arbeit in ${d} Sekunden abgeschlossen`,
|
|
221
|
+
fr: (d) => `Claude a termine en ${d} secondes`,
|
|
222
|
+
es: (d) => `Claude termino en ${d} segundos`,
|
|
223
|
+
pt: (d) => `Claude terminou em ${d} segundos`,
|
|
224
|
+
ja: (d) => `Claudeは${d}秒でコーディングを完了しました`,
|
|
225
|
+
ko: (d) => `Claude가 ${d}초 만에 코딩을 완료했습니다`,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
function getVoicePhrase (duration) {
|
|
229
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en';
|
|
230
|
+
const lang = locale.split('-')[0].toLowerCase();
|
|
231
|
+
const fn = voicePhrases[lang] || voicePhrases.en;
|
|
232
|
+
return fn(duration);
|
|
233
|
+
}
|
|
234
|
+
|
|
148
235
|
function speakResult (config, duration) {
|
|
149
236
|
if (!config.voice.enabled) {
|
|
150
237
|
return;
|
|
151
238
|
}
|
|
152
239
|
try {
|
|
153
|
-
say.speak(
|
|
240
|
+
say.speak(getVoicePhrase(duration));
|
|
154
241
|
} catch {
|
|
155
242
|
// silent fail
|
|
156
243
|
}
|
|
@@ -224,8 +311,12 @@ process.stdin.on('end', async () => {
|
|
|
224
311
|
const message =
|
|
225
312
|
`Claude finished coding\n\nProject: ${project}\nDuration: ${duration}s\nStatus: ${status}`;
|
|
226
313
|
|
|
227
|
-
|
|
228
|
-
|
|
314
|
+
state._telegramText = `\u{1F916} ${message}`;
|
|
315
|
+
await sendTelegram(config, state);
|
|
316
|
+
delete state._telegramText;
|
|
317
|
+
saveState(state);
|
|
318
|
+
|
|
319
|
+
sendWindowsNotification(config, message);
|
|
229
320
|
playSound(config);
|
|
230
321
|
speakResult(config, duration);
|
|
231
322
|
});
|
package/package.json
CHANGED