claude-notification-plugin 1.0.50 → 1.0.55
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 +23 -11
- package/bin/install.js +2 -0
- package/bin/uninstall.js +11 -0
- package/notifier/notifier.js +105 -9
- 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.55",
|
|
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,6 +99,10 @@ 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
108
|
Environment variables `TELEGRAM_TOKEN` and `TELEGRAM_CHAT_ID` override config file values.
|
|
@@ -105,15 +111,17 @@ Environment variables `TELEGRAM_TOKEN` and `TELEGRAM_CHAT_ID` override config fi
|
|
|
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
|
|
|
@@ -129,7 +137,9 @@ Add to `.claude/settings.local.json` in the project root to control channels per
|
|
|
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
|
```
|
|
@@ -146,12 +156,13 @@ To disable all notifications for a project:
|
|
|
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
package/bin/uninstall.js
CHANGED
|
@@ -46,8 +46,19 @@ for (const file of [configPath, statePath]) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Remove plugin cache (old versions left by Claude Code)
|
|
50
|
+
const pluginCacheDir = path.join(claudeDir, 'plugins', 'cache', 'bazilio-plugins', 'claude-notification-plugin');
|
|
51
|
+
let cacheRemoved = false;
|
|
52
|
+
if (fs.existsSync(pluginCacheDir)) {
|
|
53
|
+
fs.rmSync(pluginCacheDir, { recursive: true, force: true });
|
|
54
|
+
cacheRemoved = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
console.log('');
|
|
50
58
|
console.log('Claude Notification Plugin uninstalled.');
|
|
51
59
|
console.log('Hooks removed from settings.json');
|
|
52
60
|
console.log('Config files deleted.');
|
|
61
|
+
if (cacheRemoved) {
|
|
62
|
+
console.log('Plugin cache cleaned.');
|
|
63
|
+
}
|
|
53
64
|
console.log('');
|
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,6 +92,12 @@ 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
|
}
|
|
@@ -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
|
}
|
|
@@ -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.55",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|