claude-notification-plugin 1.0.52 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-notification-plugin",
3
- "version": "1.0.52",
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 | Channel |
109
- |--------------------------|----------------------------|
110
- | `CLAUDE_NOTIFY_TELEGRAM` | Telegram messages |
111
- | `CLAUDE_NOTIFY_DESKTOP` | Desktop notifications |
112
- | `CLAUDE_NOTIFY_SOUND` | Sound alert |
113
- | `CLAUDE_NOTIFY_VOICE` | Voice announcement (TTS) |
114
- | `CLAUDE_NOTIFY_WAITING` | Waiting-for-input events |
115
- | `CLAUDE_NOTIFY_DEBUG` | Debug mode |
116
- | `CLAUDE_NOTIFY_INCLUDE_LAST_CC_MESSAGE_IN_TELEGRAM` | Include last Claude 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
@@ -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,
@@ -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
- return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
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.start = Date.now();
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
- if (state.start) {
587
- duration = Math.round((Date.now() - state.start) / 1000);
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>\nDuration: ${duration}s\nTrigger: ${eventType}`;
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.52",
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": {