claude-notification-plugin 1.0.52 → 1.0.59
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/.claude-plugin/plugin.json +1 -1
- package/README.md +26 -14
- package/bin/install.js +3 -1
- package/notifier/notifier.js +111 -15
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.59",
|
|
4
4
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Viacheslav Makarov",
|
package/README.md
CHANGED
|
@@ -81,6 +81,8 @@ Config file: `~/.claude/notifier.config.json`
|
|
|
81
81
|
"voice": {
|
|
82
82
|
"enabled": true
|
|
83
83
|
},
|
|
84
|
+
"webhookUrl": "",
|
|
85
|
+
"sendUserPromptToWebhook": false,
|
|
84
86
|
"minSeconds": 15,
|
|
85
87
|
"notifyOnWaiting": false,
|
|
86
88
|
"debug": false
|
|
@@ -97,23 +99,29 @@ Each channel has an `enabled` flag (`true`/`false`) for global control.
|
|
|
97
99
|
|
|
98
100
|
`notifyOnWaiting` — send notifications when Claude is waiting for user input, e.g. permission prompts (default: `false`, set `true` to enable).
|
|
99
101
|
|
|
102
|
+
`webhookUrl` — URL to send a POST request with full notification data (JSON). If empty, no request is sent. On `Stop`/`Notification` events the payload includes `title`, `project`, `branch`, `duration`, `trigger`, `voicePhrase`, and `hookEvent` (the raw hook input). Useful for integrating with custom dashboards, logging services, or automation pipelines.
|
|
103
|
+
|
|
104
|
+
`sendUserPromptToWebhook` — also send user prompts to the webhook URL (default: `false`). When enabled, each `UserPromptSubmit` event sends a POST with `title`, `project`, `trigger`, `prompt` (user's message text), and `hookEvent`. Requires `webhookUrl` to be set.
|
|
105
|
+
|
|
100
106
|
`debug` — include extra info in notifications: voice phrase text, full hook event JSON (formatted as code block in Telegram). Default: `false`.
|
|
101
107
|
|
|
102
|
-
Environment variables `
|
|
108
|
+
Environment variables `CLAUDE_NOTIFY_TELEGRAM_TOKEN` and `CLAUDE_NOTIFY_TELEGRAM_CHAT_ID` override config file values.
|
|
103
109
|
|
|
104
110
|
### Per-channel environment variables
|
|
105
111
|
|
|
106
112
|
These env vars override the global config per channel (`"1"` = on, `"0"` = off):
|
|
107
113
|
|
|
108
|
-
| Variable
|
|
109
|
-
|
|
110
|
-
| `CLAUDE_NOTIFY_TELEGRAM`
|
|
111
|
-
| `CLAUDE_NOTIFY_DESKTOP`
|
|
112
|
-
| `CLAUDE_NOTIFY_SOUND`
|
|
113
|
-
| `CLAUDE_NOTIFY_VOICE`
|
|
114
|
-
| `CLAUDE_NOTIFY_WAITING`
|
|
115
|
-
| `CLAUDE_NOTIFY_DEBUG`
|
|
116
|
-
| `CLAUDE_NOTIFY_INCLUDE_LAST_CC_MESSAGE_IN_TELEGRAM`
|
|
114
|
+
| Variable | Channel |
|
|
115
|
+
|-------------------------------------------------------|-----------------------------------------|
|
|
116
|
+
| `CLAUDE_NOTIFY_TELEGRAM` | Telegram messages |
|
|
117
|
+
| `CLAUDE_NOTIFY_DESKTOP` | Desktop notifications |
|
|
118
|
+
| `CLAUDE_NOTIFY_SOUND` | Sound alert |
|
|
119
|
+
| `CLAUDE_NOTIFY_VOICE` | Voice announcement (TTS) |
|
|
120
|
+
| `CLAUDE_NOTIFY_WAITING` | Waiting-for-input events |
|
|
121
|
+
| `CLAUDE_NOTIFY_DEBUG` | Debug mode |
|
|
122
|
+
| `CLAUDE_NOTIFY_INCLUDE_LAST_CC_MESSAGE_IN_TELEGRAM` | Include last Claude message in Telegram |
|
|
123
|
+
| `CLAUDE_NOTIFY_WEBHOOK_URL` | Webhook URL for POST requests |
|
|
124
|
+
| `CLAUDE_NOTIFY_SEND_USER_PROMPT_TO_WEBHOOK` | Send user prompts to webhook |
|
|
117
125
|
|
|
118
126
|
### Per-project configuration
|
|
119
127
|
|
|
@@ -122,14 +130,16 @@ Add to `.claude/settings.local.json` in the project root to control channels per
|
|
|
122
130
|
```json
|
|
123
131
|
{
|
|
124
132
|
"env": {
|
|
125
|
-
"
|
|
133
|
+
"CLAUDE_NOTIFY_DISABLE": 0,
|
|
126
134
|
"CLAUDE_NOTIFY_TELEGRAM": 1,
|
|
127
135
|
"CLAUDE_NOTIFY_DESKTOP": 1,
|
|
128
136
|
"CLAUDE_NOTIFY_SOUND": 1,
|
|
129
137
|
"CLAUDE_NOTIFY_VOICE": 1,
|
|
130
138
|
"CLAUDE_NOTIFY_WAITING": 1,
|
|
131
139
|
"CLAUDE_NOTIFY_DEBUG": 0,
|
|
132
|
-
"CLAUDE_NOTIFY_INCLUDE_LAST_CC_MESSAGE_IN_TELEGRAM": 1
|
|
140
|
+
"CLAUDE_NOTIFY_INCLUDE_LAST_CC_MESSAGE_IN_TELEGRAM": 1,
|
|
141
|
+
"CLAUDE_NOTIFY_WEBHOOK_URL": "",
|
|
142
|
+
"CLAUDE_NOTIFY_SEND_USER_PROMPT_TO_WEBHOOK": 0
|
|
133
143
|
}
|
|
134
144
|
}
|
|
135
145
|
```
|
|
@@ -139,19 +149,20 @@ To disable all notifications for a project:
|
|
|
139
149
|
```json
|
|
140
150
|
{
|
|
141
151
|
"env": {
|
|
142
|
-
"
|
|
152
|
+
"CLAUDE_NOTIFY_DISABLE": "1"
|
|
143
153
|
}
|
|
144
154
|
}
|
|
145
155
|
```
|
|
146
156
|
|
|
147
157
|
## Notification format
|
|
148
158
|
|
|
149
|
-
Notifications include project name, duration, and the trigger event:
|
|
159
|
+
Notifications include project name, git branch (when available), duration, and the trigger event:
|
|
150
160
|
|
|
151
161
|
```
|
|
152
162
|
🤖 Claude finished coding
|
|
153
163
|
|
|
154
164
|
Project: my-project
|
|
165
|
+
Branch: feature-auth
|
|
155
166
|
Duration: 45s
|
|
156
167
|
Trigger: Stop
|
|
157
168
|
```
|
|
@@ -162,6 +173,7 @@ When Claude is waiting for input (and `notifyOnWaiting` is enabled):
|
|
|
162
173
|
🤖 Claude waiting for input
|
|
163
174
|
|
|
164
175
|
Project: my-project
|
|
176
|
+
Branch: feature-auth
|
|
165
177
|
Duration: 30s
|
|
166
178
|
Trigger: Notification
|
|
167
179
|
```
|
package/bin/install.js
CHANGED
|
@@ -161,6 +161,8 @@ async function main () {
|
|
|
161
161
|
voice: {
|
|
162
162
|
enabled: true,
|
|
163
163
|
},
|
|
164
|
+
webhookUrl: '',
|
|
165
|
+
sendUserPromptToWebhook: false,
|
|
164
166
|
minSeconds: 15,
|
|
165
167
|
notifyOnWaiting: false,
|
|
166
168
|
debug: false,
|
|
@@ -235,7 +237,7 @@ async function main () {
|
|
|
235
237
|
|
|
236
238
|
console.log('');
|
|
237
239
|
console.log('To disable per project, add to .claude/settings.local.json:');
|
|
238
|
-
console.log(' { "env": { "
|
|
240
|
+
console.log(' { "env": { "CLAUDE_NOTIFY_DISABLE": "1" } }');
|
|
239
241
|
console.log('');
|
|
240
242
|
}
|
|
241
243
|
|
package/notifier/notifier.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import process from 'process';
|
|
7
|
-
import { spawn } from 'child_process';
|
|
7
|
+
import { execSync, spawn } from 'child_process';
|
|
8
8
|
|
|
9
9
|
// ----------------------
|
|
10
10
|
// CONFIG
|
|
@@ -26,6 +26,19 @@ function debugLog (config, ...args) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function getBranch (cwd) {
|
|
30
|
+
try {
|
|
31
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
32
|
+
cwd,
|
|
33
|
+
encoding: 'utf-8',
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
timeout: 3000,
|
|
36
|
+
}).trim();
|
|
37
|
+
} catch {
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
function loadConfig () {
|
|
30
43
|
const configPath = path.join(os.homedir(), '.claude', 'notifier.config.json');
|
|
31
44
|
|
|
@@ -47,6 +60,8 @@ function loadConfig () {
|
|
|
47
60
|
voice: {
|
|
48
61
|
enabled: true,
|
|
49
62
|
},
|
|
63
|
+
webhookUrl: '',
|
|
64
|
+
sendUserPromptToWebhook: false,
|
|
50
65
|
minSeconds: 15,
|
|
51
66
|
notifyOnWaiting: false,
|
|
52
67
|
debug: false,
|
|
@@ -77,16 +92,22 @@ function loadConfig () {
|
|
|
77
92
|
if (typeof user.debug === 'boolean') {
|
|
78
93
|
config.debug = user.debug;
|
|
79
94
|
}
|
|
95
|
+
if (typeof user.webhookUrl === 'string') {
|
|
96
|
+
config.webhookUrl = user.webhookUrl;
|
|
97
|
+
}
|
|
98
|
+
if (typeof user.sendUserPromptToWebhook === 'boolean') {
|
|
99
|
+
config.sendUserPromptToWebhook = user.sendUserPromptToWebhook;
|
|
100
|
+
}
|
|
80
101
|
} catch {
|
|
81
102
|
// ignore malformed config
|
|
82
103
|
}
|
|
83
104
|
}
|
|
84
105
|
|
|
85
|
-
if (process.env.
|
|
86
|
-
config.telegram.token = process.env.
|
|
106
|
+
if (process.env.CLAUDE_NOTIFY_TELEGRAM_TOKEN) {
|
|
107
|
+
config.telegram.token = process.env.CLAUDE_NOTIFY_TELEGRAM_TOKEN;
|
|
87
108
|
}
|
|
88
|
-
if (process.env.
|
|
89
|
-
config.telegram.chatId = process.env.
|
|
109
|
+
if (process.env.CLAUDE_NOTIFY_TELEGRAM_CHAT_ID) {
|
|
110
|
+
config.telegram.chatId = process.env.CLAUDE_NOTIFY_TELEGRAM_CHAT_ID;
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
// Per-channel env overrides (0 = off, 1 = on)
|
|
@@ -111,6 +132,12 @@ function loadConfig () {
|
|
|
111
132
|
if (process.env.CLAUDE_NOTIFY_INCLUDE_LAST_CC_MESSAGE_IN_TELEGRAM !== undefined) {
|
|
112
133
|
config.telegram.includeLastCcMessageInTelegram = process.env.CLAUDE_NOTIFY_INCLUDE_LAST_CC_MESSAGE_IN_TELEGRAM === '1';
|
|
113
134
|
}
|
|
135
|
+
if (process.env.CLAUDE_NOTIFY_WEBHOOK_URL) {
|
|
136
|
+
config.webhookUrl = process.env.CLAUDE_NOTIFY_WEBHOOK_URL;
|
|
137
|
+
}
|
|
138
|
+
if (process.env.CLAUDE_NOTIFY_SEND_USER_PROMPT_TO_WEBHOOK !== undefined) {
|
|
139
|
+
config.sendUserPromptToWebhook = process.env.CLAUDE_NOTIFY_SEND_USER_PROMPT_TO_WEBHOOK === '1';
|
|
140
|
+
}
|
|
114
141
|
|
|
115
142
|
return config;
|
|
116
143
|
}
|
|
@@ -120,8 +147,8 @@ function loadConfig () {
|
|
|
120
147
|
// ----------------------
|
|
121
148
|
|
|
122
149
|
function isNotifierDisabled () {
|
|
123
|
-
return process.env.
|
|
124
|
-
|| process.env.
|
|
150
|
+
return process.env.CLAUDE_NOTIFY_DISABLE === '1'
|
|
151
|
+
|| process.env.CLAUDE_NOTIFY_DISABLE === 'true';
|
|
125
152
|
}
|
|
126
153
|
|
|
127
154
|
// ----------------------
|
|
@@ -137,12 +164,23 @@ const STATE_FILE = path.join(
|
|
|
137
164
|
function loadState () {
|
|
138
165
|
if (fs.existsSync(STATE_FILE)) {
|
|
139
166
|
try {
|
|
140
|
-
|
|
167
|
+
const raw = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
168
|
+
// Migrate flat state (pre-session format) to new format
|
|
169
|
+
if (!raw.sessions && raw.start !== undefined) {
|
|
170
|
+
return { sessions: {}, sentMessages: raw.sentMessages || [] };
|
|
171
|
+
}
|
|
172
|
+
if (!raw.sessions) {
|
|
173
|
+
raw.sessions = {};
|
|
174
|
+
}
|
|
175
|
+
if (!raw.sentMessages) {
|
|
176
|
+
raw.sentMessages = [];
|
|
177
|
+
}
|
|
178
|
+
return raw;
|
|
141
179
|
} catch {
|
|
142
|
-
return {};
|
|
180
|
+
return { sessions: {}, sentMessages: [] };
|
|
143
181
|
}
|
|
144
182
|
}
|
|
145
|
-
return {};
|
|
183
|
+
return { sessions: {}, sentMessages: [] };
|
|
146
184
|
}
|
|
147
185
|
|
|
148
186
|
function saveState (state) {
|
|
@@ -151,6 +189,16 @@ function saveState (state) {
|
|
|
151
189
|
fs.writeFileSync(STATE_FILE, JSON.stringify(state));
|
|
152
190
|
}
|
|
153
191
|
|
|
192
|
+
function cleanStaleSessions (state) {
|
|
193
|
+
const maxAge = 24 * 3600_000;
|
|
194
|
+
const now = Date.now();
|
|
195
|
+
for (const sid of Object.keys(state.sessions)) {
|
|
196
|
+
if (now - state.sessions[sid].start > maxAge) {
|
|
197
|
+
delete state.sessions[sid];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
154
202
|
// ----------------------
|
|
155
203
|
// TELEGRAM
|
|
156
204
|
// ----------------------
|
|
@@ -286,6 +334,25 @@ async function sendTelegram (config, state) {
|
|
|
286
334
|
}
|
|
287
335
|
}
|
|
288
336
|
|
|
337
|
+
// ----------------------
|
|
338
|
+
// WEBHOOK
|
|
339
|
+
// ----------------------
|
|
340
|
+
|
|
341
|
+
async function sendWebhook (config, payload) {
|
|
342
|
+
if (!config.webhookUrl) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await fetch(config.webhookUrl, {
|
|
347
|
+
method: 'POST',
|
|
348
|
+
headers: { 'Content-Type': 'application/json' },
|
|
349
|
+
body: JSON.stringify(payload),
|
|
350
|
+
});
|
|
351
|
+
} catch (err) {
|
|
352
|
+
debugLog(config, 'sendWebhook failed:', err.message);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
289
356
|
// ----------------------
|
|
290
357
|
// DESKTOP NOTIFICATION
|
|
291
358
|
// ----------------------
|
|
@@ -553,20 +620,31 @@ process.stdin.on('end', async () => {
|
|
|
553
620
|
const eventType = event.hook_event_name || 'unknown';
|
|
554
621
|
const cwd = event.cwd || process.cwd();
|
|
555
622
|
const project = path.basename(cwd);
|
|
623
|
+
const sessionId = event.session_id || 'default';
|
|
556
624
|
|
|
557
625
|
if (isNotifierDisabled()) {
|
|
558
626
|
process.exit(0);
|
|
559
627
|
}
|
|
560
628
|
|
|
561
629
|
const state = loadState();
|
|
630
|
+
cleanStaleSessions(state);
|
|
562
631
|
|
|
563
632
|
// ----------------------
|
|
564
633
|
// START TIMER
|
|
565
634
|
// ----------------------
|
|
566
635
|
|
|
567
636
|
if (eventType === 'UserPromptSubmit') {
|
|
568
|
-
state.
|
|
637
|
+
state.sessions[sessionId] = { start: Date.now() };
|
|
569
638
|
saveState(state);
|
|
639
|
+
if (config.sendUserPromptToWebhook) {
|
|
640
|
+
await sendWebhook(config, {
|
|
641
|
+
title: 'User prompt submitted',
|
|
642
|
+
project,
|
|
643
|
+
trigger: eventType,
|
|
644
|
+
prompt: event.prompt || '',
|
|
645
|
+
hookEvent: event,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
570
648
|
process.exit(0);
|
|
571
649
|
}
|
|
572
650
|
|
|
@@ -583,8 +661,9 @@ process.stdin.on('end', async () => {
|
|
|
583
661
|
}
|
|
584
662
|
|
|
585
663
|
let duration = 0;
|
|
586
|
-
|
|
587
|
-
|
|
664
|
+
const session = state.sessions[sessionId];
|
|
665
|
+
if (session?.start) {
|
|
666
|
+
duration = Math.round((Date.now() - session.start) / 1000);
|
|
588
667
|
}
|
|
589
668
|
|
|
590
669
|
if (duration < config.minSeconds) {
|
|
@@ -595,11 +674,15 @@ process.stdin.on('end', async () => {
|
|
|
595
674
|
? 'Claude waiting for input'
|
|
596
675
|
: 'Claude finished coding';
|
|
597
676
|
|
|
677
|
+
const branch = getBranch(cwd);
|
|
678
|
+
const branchLine = branch ? `\nBranch: ${branch}` : '';
|
|
679
|
+
const branchLineHtml = branch ? `\nBranch: <b>${escapeHtml(branch)}</b>` : '';
|
|
680
|
+
|
|
598
681
|
let message =
|
|
599
|
-
`${title}\n\nProject: ${project}\nDuration: ${duration}s\nTrigger: ${eventType}`;
|
|
682
|
+
`${title}\n\nProject: ${project}${branchLine}\nDuration: ${duration}s\nTrigger: ${eventType}`;
|
|
600
683
|
|
|
601
684
|
let telegramMessage =
|
|
602
|
-
`${escapeHtml(title)}\n\nProject: <b>${escapeHtml(project)}</b
|
|
685
|
+
`${escapeHtml(title)}\n\nProject: <b>${escapeHtml(project)}</b>${branchLineHtml}\nDuration: ${duration}s\nTrigger: ${eventType}`;
|
|
603
686
|
|
|
604
687
|
if (config.telegram.includeLastCcMessageInTelegram && event.last_assistant_message) {
|
|
605
688
|
const maxLen = 3500;
|
|
@@ -621,9 +704,22 @@ process.stdin.on('end', async () => {
|
|
|
621
704
|
telegramMessage += debugBlockHtml;
|
|
622
705
|
}
|
|
623
706
|
|
|
707
|
+
await sendWebhook(config, {
|
|
708
|
+
title,
|
|
709
|
+
project,
|
|
710
|
+
branch: branch || undefined,
|
|
711
|
+
duration,
|
|
712
|
+
trigger: eventType,
|
|
713
|
+
voicePhrase: config.voice.enabled ? getVoicePhrase(duration) : null,
|
|
714
|
+
hookEvent: event,
|
|
715
|
+
});
|
|
716
|
+
|
|
624
717
|
state._telegramText = `\u{1F916} ${telegramMessage}`;
|
|
625
718
|
await sendTelegram(config, state);
|
|
626
719
|
delete state._telegramText;
|
|
720
|
+
if (eventType === 'Stop') {
|
|
721
|
+
delete state.sessions[sessionId];
|
|
722
|
+
}
|
|
627
723
|
saveState(state);
|
|
628
724
|
|
|
629
725
|
await sendDesktopNotification(config, message);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
3
|
"productName": "claude-notification-plugin",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.59",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|