claude-notification-plugin 1.1.52 → 1.1.56
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 +38 -34
- package/commit-sha +1 -1
- package/hooks/hooks.json +89 -34
- package/listener/LISTENER-DETAILED.md +117 -94
- package/listener/listener.js +84 -50
- package/listener/message-parser.js +96 -111
- package/listener/pty-runner.js +122 -98
- package/listener/telegram-poller.js +21 -4
- package/notifier/NOTIFIER-DETAILED.md +82 -0
- package/notifier/notifier.js +88 -19
- package/package.json +1 -1
package/listener/pty-runner.js
CHANGED
|
@@ -68,12 +68,9 @@ export class PtyRunner extends EventEmitter {
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Check for new marker files in the signal directory.
|
|
71
|
+
* Handles typed signals: stop (default), error, ready, activity, compact.
|
|
71
72
|
*/
|
|
72
73
|
_checkMarkerFiles () {
|
|
73
|
-
if (this.pendingMarkers.size === 0) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
74
|
let files;
|
|
78
75
|
try {
|
|
79
76
|
files = fs.readdirSync(PTY_SIGNAL_DIR);
|
|
@@ -94,26 +91,96 @@ export class PtyRunner extends EventEmitter {
|
|
|
94
91
|
continue;
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
// Try to match by cwd (primary matching for PTY runner)
|
|
98
94
|
const cwd = marker.cwd;
|
|
99
|
-
if (cwd) {
|
|
95
|
+
if (!cwd) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const type = marker.type || 'stop';
|
|
100
|
+
|
|
101
|
+
if (type === 'stop') {
|
|
102
|
+
// Completion signal — resolve pending marker
|
|
103
|
+
if (this.pendingMarkers.size === 0) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
100
106
|
for (const [pid, resolve] of this.pendingMarkers) {
|
|
101
107
|
const session = this._findSessionByPendingId(pid);
|
|
102
108
|
if (session && this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
|
|
103
109
|
this.pendingMarkers.delete(pid);
|
|
104
|
-
|
|
105
|
-
fs.unlinkSync(filePath);
|
|
106
|
-
} catch {
|
|
107
|
-
// ignore
|
|
108
|
-
}
|
|
110
|
+
this._unlinkSafe(filePath);
|
|
109
111
|
resolve(marker);
|
|
110
112
|
break;
|
|
111
113
|
}
|
|
112
114
|
}
|
|
115
|
+
} else if (type === 'error') {
|
|
116
|
+
// StopFailure — emit error, abort task
|
|
117
|
+
this._unlinkSafe(filePath);
|
|
118
|
+
for (const [workDir, session] of this.sessions) {
|
|
119
|
+
if (session.state === 'busy' && this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
|
|
120
|
+
if (session._pendingId && this.pendingMarkers.has(session._pendingId)) {
|
|
121
|
+
this.pendingMarkers.delete(session._pendingId);
|
|
122
|
+
}
|
|
123
|
+
const task = session.currentTask;
|
|
124
|
+
session.state = 'idle';
|
|
125
|
+
session.currentTask = null;
|
|
126
|
+
this._destroyPty(workDir);
|
|
127
|
+
const errorMsg = `API error: ${marker.error}${marker.errorDetails ? ' — ' + marker.errorDetails : ''}`;
|
|
128
|
+
this.logger.error(`Hook signal: ${errorMsg} in ${workDir}`);
|
|
129
|
+
if (this.taskLogger) {
|
|
130
|
+
this.taskLogger.logAnswer(task?.project || 'unknown', task?.branch || 'main', errorMsg, 1);
|
|
131
|
+
}
|
|
132
|
+
this.emit('error', workDir, task, errorMsg);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} else if (type === 'ready') {
|
|
137
|
+
// SessionStart — emit ready event
|
|
138
|
+
this._unlinkSafe(filePath);
|
|
139
|
+
for (const [workDir, session] of this.sessions) {
|
|
140
|
+
if (this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
|
|
141
|
+
session._model = marker.model || '';
|
|
142
|
+
this.emit('ready', workDir, marker);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else if (type === 'activity') {
|
|
147
|
+
// PostToolUse — update activity data (don't delete, gets overwritten)
|
|
148
|
+
for (const [, session] of this.sessions) {
|
|
149
|
+
if (this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
|
|
150
|
+
session._lastActivity = {
|
|
151
|
+
toolName: marker.toolName,
|
|
152
|
+
toolInput: marker.toolInput,
|
|
153
|
+
timestamp: marker.timestamp,
|
|
154
|
+
};
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} else if (type === 'compact') {
|
|
159
|
+
// PostCompact — update compaction info
|
|
160
|
+
this._unlinkSafe(filePath);
|
|
161
|
+
for (const [workDir, session] of this.sessions) {
|
|
162
|
+
if (this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
|
|
163
|
+
session._lastCompact = {
|
|
164
|
+
summary: marker.summary,
|
|
165
|
+
trigger: marker.trigger,
|
|
166
|
+
timestamp: marker.timestamp,
|
|
167
|
+
};
|
|
168
|
+
this.emit('compact', workDir, marker);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
113
172
|
}
|
|
114
173
|
}
|
|
115
174
|
}
|
|
116
175
|
|
|
176
|
+
_unlinkSafe (filePath) {
|
|
177
|
+
try {
|
|
178
|
+
fs.unlinkSync(filePath);
|
|
179
|
+
} catch {
|
|
180
|
+
// ignore
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
117
184
|
_normalizePath (p) {
|
|
118
185
|
return p.replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
119
186
|
}
|
|
@@ -261,65 +328,11 @@ export class PtyRunner extends EventEmitter {
|
|
|
261
328
|
writeLines();
|
|
262
329
|
this.logger.info(`PTY task sent to ${workDir}: ${task.text.slice(0, 100)}`);
|
|
263
330
|
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
// Claude CLI status bar while Claude is still actively working. To avoid false
|
|
267
|
-
// positives we require the buffer to stop growing for two consecutive checks
|
|
268
|
-
// before treating the match as a real error.
|
|
269
|
-
const errorPatterns = [
|
|
270
|
-
{ pattern: 'auto mode temporarily unavailable', msg: 'Claude auto mode temporarily unavailable — retry later' },
|
|
271
|
-
{ pattern: 'Session expired', msg: 'Claude session expired' },
|
|
272
|
-
{ pattern: 'Authentication required', msg: 'Claude authentication required' },
|
|
273
|
-
];
|
|
274
|
-
let errorCandidate = null; // { msg, bufLen } — suspected error awaiting confirmation
|
|
275
|
-
let checkedUpTo = 0; // buffer offset already scanned — avoids re-matching the same text
|
|
276
|
-
const errorCheckInterval = setInterval(() => {
|
|
277
|
-
const buf = session._buffer || '';
|
|
278
|
-
const bufLen = buf.length;
|
|
279
|
-
|
|
280
|
-
// If we have a candidate from the previous cycle, confirm it
|
|
281
|
-
if (errorCandidate) {
|
|
282
|
-
if (bufLen === errorCandidate.bufLen) {
|
|
283
|
-
// Buffer did not grow — Claude stopped, treat as real error
|
|
284
|
-
clearInterval(errorCheckInterval);
|
|
285
|
-
this.logger.error(`PTY fatal: ${errorCandidate.msg} in ${workDir}`);
|
|
286
|
-
if (session._pendingId && this.pendingMarkers.has(session._pendingId)) {
|
|
287
|
-
this.pendingMarkers.delete(session._pendingId);
|
|
288
|
-
}
|
|
289
|
-
session.state = 'idle';
|
|
290
|
-
session.currentTask = null;
|
|
291
|
-
this._destroyPty(workDir);
|
|
292
|
-
this.emit('error', workDir, task, errorCandidate.msg);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
// Buffer grew — Claude is still working, false positive
|
|
296
|
-
this.logger.info(`PTY error candidate dismissed (buffer grew) in ${workDir}`);
|
|
297
|
-
errorCandidate = null;
|
|
298
|
-
checkedUpTo = bufLen;
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Scan only new buffer content for pattern matches
|
|
303
|
-
if (bufLen <= checkedUpTo) {
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
const fresh = buf.slice(checkedUpTo);
|
|
307
|
-
for (const { pattern, msg } of errorPatterns) {
|
|
308
|
-
if (fresh.includes(pattern)) {
|
|
309
|
-
this.logger.warn(`PTY error candidate detected: "${pattern}" in ${workDir}, waiting to confirm…`);
|
|
310
|
-
errorCandidate = { msg, bufLen };
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
checkedUpTo = bufLen;
|
|
315
|
-
}, 3000);
|
|
316
|
-
|
|
317
|
-
// Clean up error monitor when task completes normally
|
|
318
|
-
const clearErrorCheck = () => clearInterval(errorCheckInterval);
|
|
331
|
+
// Error detection is now handled by the StopFailure hook signal,
|
|
332
|
+
// which writes an error signal file processed by _checkMarkerFiles.
|
|
319
333
|
|
|
320
334
|
// Handle completion asynchronously
|
|
321
335
|
markerPromise.then((marker) => {
|
|
322
|
-
clearErrorCheck();
|
|
323
336
|
session.state = 'idle';
|
|
324
337
|
session.currentTask = null;
|
|
325
338
|
session.sessionId = marker.sessionId;
|
|
@@ -341,7 +354,6 @@ export class PtyRunner extends EventEmitter {
|
|
|
341
354
|
}
|
|
342
355
|
this.emit('complete', workDir, task, result);
|
|
343
356
|
}).catch((err) => {
|
|
344
|
-
clearErrorCheck();
|
|
345
357
|
session.state = 'idle';
|
|
346
358
|
session.currentTask = null;
|
|
347
359
|
|
|
@@ -381,8 +393,8 @@ export class PtyRunner extends EventEmitter {
|
|
|
381
393
|
// Reduce PTY output noise: disable animations, progress bar, tips
|
|
382
394
|
if (!args.includes('--settings')) {
|
|
383
395
|
args.push('--settings', JSON.stringify({
|
|
384
|
-
prefersReducedMotion: true,
|
|
385
|
-
outputStyle: 'plain',
|
|
396
|
+
// prefersReducedMotion: true,
|
|
397
|
+
// outputStyle: 'plain',
|
|
386
398
|
terminalProgressBarEnabled: false,
|
|
387
399
|
spinnerTipsEnabled: false,
|
|
388
400
|
showTurnDuration: false,
|
|
@@ -418,44 +430,18 @@ export class PtyRunner extends EventEmitter {
|
|
|
418
430
|
_buffer: '',
|
|
419
431
|
};
|
|
420
432
|
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
let lastPermissionAnswerAt = 0;
|
|
424
|
-
let lastPermissionAnswerTime = 0;
|
|
425
|
-
const PERMISSION_COOLDOWN_MS = 5000;
|
|
433
|
+
// Permission auto-approval is now handled by the PermissionRequest hook
|
|
434
|
+
// (returns auto-approve JSON when CLAUDE_NOTIFY_FROM_LISTENER=1).
|
|
426
435
|
|
|
427
436
|
ptyProcess.onData((data) => {
|
|
428
437
|
session._buffer += data;
|
|
429
438
|
// Keep buffer reasonable size
|
|
430
439
|
if (session._buffer.length > 50000) {
|
|
431
440
|
session._buffer = session._buffer.slice(-25000);
|
|
432
|
-
lastPermissionAnswerAt = Math.max(0, lastPermissionAnswerAt - 25000);
|
|
433
441
|
}
|
|
434
442
|
if (session._logStream) {
|
|
435
443
|
session._logStream.write(data);
|
|
436
444
|
}
|
|
437
|
-
|
|
438
|
-
// Auto-answer interactive permission prompts that appear when
|
|
439
|
-
// "auto mode temporarily unavailable". The prompt looks like:
|
|
440
|
-
// Do you want to proceed?
|
|
441
|
-
// ❯ 1. Yes
|
|
442
|
-
// 2. Yes, and always allow …
|
|
443
|
-
// 3. No
|
|
444
|
-
// We send Enter to approve (cursor ❯ is already on "1. Yes").
|
|
445
|
-
// Cooldown prevents double-fire from screen redraws.
|
|
446
|
-
const now = Date.now();
|
|
447
|
-
if (session.state === 'busy'
|
|
448
|
-
&& session._buffer.length - lastPermissionAnswerAt > 50
|
|
449
|
-
&& now - lastPermissionAnswerTime > PERMISSION_COOLDOWN_MS) {
|
|
450
|
-
const tail = session._buffer.slice(lastPermissionAnswerAt);
|
|
451
|
-
const clean = tail.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\s/g, '');
|
|
452
|
-
if (clean.includes('Doyouwanttoproceed') && clean.includes('1.Yes')) {
|
|
453
|
-
lastPermissionAnswerAt = session._buffer.length;
|
|
454
|
-
lastPermissionAnswerTime = now;
|
|
455
|
-
this.logger.info(`Auto-answering permission prompt in ${session.workDir}`);
|
|
456
|
-
session.pty.write('\r');
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
445
|
});
|
|
460
446
|
|
|
461
447
|
ptyProcess.onExit(({ exitCode }) => {
|
|
@@ -609,6 +595,44 @@ export class PtyRunner extends EventEmitter {
|
|
|
609
595
|
return session?._buffer || '';
|
|
610
596
|
}
|
|
611
597
|
|
|
598
|
+
/**
|
|
599
|
+
* Get last tool activity for a workDir (from PostToolUse hook signals).
|
|
600
|
+
*/
|
|
601
|
+
getActivity (workDir) {
|
|
602
|
+
const session = this.sessions.get(workDir);
|
|
603
|
+
return session?._lastActivity || null;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Clean up activity signal file for a workDir.
|
|
608
|
+
*/
|
|
609
|
+
cleanActivitySignal (workDir) {
|
|
610
|
+
const session = this.sessions.get(workDir);
|
|
611
|
+
if (session) {
|
|
612
|
+
session._lastActivity = null;
|
|
613
|
+
}
|
|
614
|
+
// Also try to delete any activity files matching this workDir
|
|
615
|
+
try {
|
|
616
|
+
const files = fs.readdirSync(PTY_SIGNAL_DIR);
|
|
617
|
+
for (const f of files) {
|
|
618
|
+
if (!f.startsWith('act_') || !f.endsWith('.json')) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
const filePath = path.join(PTY_SIGNAL_DIR, f);
|
|
622
|
+
try {
|
|
623
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
624
|
+
if (data.cwd && this._normalizePath(data.cwd) === this._normalizePath(workDir)) {
|
|
625
|
+
fs.unlinkSync(filePath);
|
|
626
|
+
}
|
|
627
|
+
} catch {
|
|
628
|
+
// ignore
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
} catch {
|
|
632
|
+
// ignore
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
612
636
|
/**
|
|
613
637
|
* Get diagnostic info about a PTY session.
|
|
614
638
|
*/
|
|
@@ -276,17 +276,34 @@ function splitMessage (text) {
|
|
|
276
276
|
|
|
277
277
|
// Strip ANSI escape codes and terminal control sequences from PTY output
|
|
278
278
|
function stripAnsi (text) {
|
|
279
|
-
|
|
279
|
+
let result = text
|
|
280
|
+
// Cursor-right (\x1b[<N>C) → replace with N spaces (preserves word spacing)
|
|
281
|
+
.replace(/\x1b\[(\d+)C/g, (_, n) => ' '.repeat(parseInt(n, 10)))
|
|
282
|
+
// Cursor-position (\x1b[<row>;<col>H) → replace with newline (absolute move = new line)
|
|
283
|
+
.replace(/\x1b\[\d+;\d+H/g, '\n')
|
|
280
284
|
// CSI sequences: \x1b[ followed by optional ?/>/! prefix, params, and terminator
|
|
281
285
|
.replace(/\x1b\[[?>=!]?[0-9;]*[a-zA-Z~]/g, '')
|
|
282
286
|
// OSC sequences: \x1b] ... (terminated by BEL or ST)
|
|
283
287
|
.replace(/\x1b][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
284
288
|
// Other two-char escape sequences (\x1b followed by any single char)
|
|
285
289
|
.replace(/\x1b[^[\]]/g, '')
|
|
286
|
-
//
|
|
287
|
-
.replace(/\r/g, '')
|
|
288
|
-
// Remaining control chars except newline and tab
|
|
290
|
+
// Remaining control chars except newline, tab, and CR
|
|
289
291
|
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
|
|
292
|
+
|
|
293
|
+
// Normalize \r\n to \n first, then simulate standalone \r (line overwrite)
|
|
294
|
+
result = result.replace(/\r\n/g, '\n');
|
|
295
|
+
const lines = result.split('\n');
|
|
296
|
+
const resolved = [];
|
|
297
|
+
for (const line of lines) {
|
|
298
|
+
if (line.includes('\r')) {
|
|
299
|
+
// Standalone \r means overwrite — keep only the last segment
|
|
300
|
+
const parts = line.split('\r');
|
|
301
|
+
resolved.push(parts[parts.length - 1]);
|
|
302
|
+
} else {
|
|
303
|
+
resolved.push(line);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return resolved.join('\n');
|
|
290
307
|
}
|
|
291
308
|
|
|
292
309
|
// Clean PTY output for display: strip ANSI + remove Claude Code UI chrome
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Notifier Hook Events - Detailed Guide
|
|
2
|
+
|
|
3
|
+
The notifier (`notifier/notifier.js`) is the plugin's hook handler — a single script invoked by Claude Code for every registered hook event. It reads JSON from stdin, determines the event type and operating mode, and dispatches accordingly.
|
|
4
|
+
|
|
5
|
+
## Operating modes
|
|
6
|
+
|
|
7
|
+
| Mode | Condition | Behavior |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **Disabled** | `CLAUDE_NOTIFY_DISABLE=1` | Exit immediately |
|
|
10
|
+
| **Listener-only** | `CLAUDE_NOTIFY_FROM_LISTENER=1` (and `CLAUDE_NOTIFY_AFTER_LISTENER` not `1`) | Write signal files to `~/.claude/pty-signals/`, no user notifications |
|
|
11
|
+
| **Normal** | Default | Send notifications (Telegram, desktop, sound, voice, webhook) |
|
|
12
|
+
|
|
13
|
+
## Hook events
|
|
14
|
+
|
|
15
|
+
| Hook Event | Mode | Sync | Purpose |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `UserPromptSubmit` | Normal | sync | Starts the notification timer (records session start time) |
|
|
18
|
+
| `Stop` | Both | sync | Normal: sends completion notification. Listener: writes completion signal file |
|
|
19
|
+
| `StopFailure` | Both | async | Normal: sends error notification. Listener: writes error signal file |
|
|
20
|
+
| `Notification` | Normal | sync | Sends waiting-for-input notification (when `notifyOnWaiting` is enabled) |
|
|
21
|
+
| `SessionStart` | Listener | async | Writes session ready signal (model name, startup/resume source) |
|
|
22
|
+
| `PermissionRequest` | Listener | sync | Auto-approves permission prompts via JSON output to stdout |
|
|
23
|
+
| `PostToolUse` | Listener | async | Writes tool activity signal (tool name, input parameters) |
|
|
24
|
+
| `PostCompact` | Listener | async | Writes context compaction signal (summary, trigger type) |
|
|
25
|
+
|
|
26
|
+
**Sync** hooks block Claude until the script completes (Claude waits for the response).
|
|
27
|
+
**Async** hooks run in the background (Claude continues immediately).
|
|
28
|
+
|
|
29
|
+
## Normal mode flow
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
UserPromptSubmit
|
|
33
|
+
→ Record session start time in state file
|
|
34
|
+
→ Send webhook (if configured)
|
|
35
|
+
|
|
36
|
+
Stop / StopFailure / Notification
|
|
37
|
+
→ Check elapsed time since session start
|
|
38
|
+
→ Skip if duration < notifyAfterSeconds (default 15s)
|
|
39
|
+
→ Build notification text (project, branch, duration, last message)
|
|
40
|
+
→ Send: Telegram, desktop toast, sound, voice, webhook
|
|
41
|
+
→ Clean up old Telegram messages (deleteAfterHours)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Listener-only mode flow
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
PermissionRequest
|
|
48
|
+
→ Output auto-approve JSON to stdout:
|
|
49
|
+
{ hookSpecificOutput: { hookEventName: "PermissionRequest",
|
|
50
|
+
decision: { behavior: "allow" } } }
|
|
51
|
+
|
|
52
|
+
Stop
|
|
53
|
+
→ Write ~/.claude/pty-signals/{sessionId}.json
|
|
54
|
+
{ sessionId, cwd, lastAssistantMessage, cost, numTurns, durationMs }
|
|
55
|
+
|
|
56
|
+
StopFailure
|
|
57
|
+
→ Write ~/.claude/pty-signals/err_{sessionId}.json
|
|
58
|
+
{ type: "error", cwd, error, errorDetails, lastAssistantMessage }
|
|
59
|
+
|
|
60
|
+
SessionStart
|
|
61
|
+
→ Write ~/.claude/pty-signals/rdy_{sessionId}.json
|
|
62
|
+
{ type: "ready", cwd, model, source }
|
|
63
|
+
|
|
64
|
+
PostToolUse
|
|
65
|
+
→ Write ~/.claude/pty-signals/act_{sessionId}.json (overwritten each call)
|
|
66
|
+
{ type: "activity", cwd, toolName, toolInput }
|
|
67
|
+
|
|
68
|
+
PostCompact
|
|
69
|
+
→ Write ~/.claude/pty-signals/cmp_{sessionId}.json
|
|
70
|
+
{ type: "compact", cwd, summary, trigger }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
See the main [README](../README.md) for configuration options.
|
|
76
|
+
|
|
77
|
+
Key settings affecting notifier behavior:
|
|
78
|
+
- `notifyAfterSeconds` (default: 15) — minimum task duration to trigger notifications
|
|
79
|
+
- `notifyOnWaiting` (default: false) — send notifications for idle/waiting events
|
|
80
|
+
- `telegram.includeLastCcMessageInTelegram` (default: true) — include Claude's last message in Telegram notification
|
|
81
|
+
- `telegram.deleteAfterHours` (default: 24) — auto-delete old notification messages
|
|
82
|
+
- `debug` (default: false) — include hook event JSON and trigger type in notifications
|
package/notifier/notifier.js
CHANGED
|
@@ -158,26 +158,73 @@ function isNotifierDisabled () {
|
|
|
158
158
|
return false;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
function
|
|
162
|
-
const sessionId = event.session_id || 'unknown';
|
|
163
|
-
const cwd = event.cwd || process.cwd();
|
|
161
|
+
function writeSignalFile (name, data) {
|
|
164
162
|
try {
|
|
165
163
|
fs.mkdirSync(PTY_SIGNAL_DIR, { recursive: true });
|
|
166
|
-
|
|
167
|
-
fs.writeFileSync(signalFile, JSON.stringify({
|
|
168
|
-
sessionId,
|
|
169
|
-
cwd,
|
|
170
|
-
lastAssistantMessage: event.last_assistant_message || '',
|
|
171
|
-
cost: event.total_cost_usd || 0,
|
|
172
|
-
numTurns: event.num_turns || 0,
|
|
173
|
-
durationMs: event.duration_ms || 0,
|
|
174
|
-
timestamp: Date.now(),
|
|
175
|
-
}));
|
|
164
|
+
fs.writeFileSync(path.join(PTY_SIGNAL_DIR, name), JSON.stringify(data));
|
|
176
165
|
} catch {
|
|
177
166
|
// silent fail
|
|
178
167
|
}
|
|
179
168
|
}
|
|
180
169
|
|
|
170
|
+
function writePtySignalFile (event) {
|
|
171
|
+
const sessionId = event.session_id || 'unknown';
|
|
172
|
+
writeSignalFile(`${sessionId}.json`, {
|
|
173
|
+
sessionId,
|
|
174
|
+
cwd: event.cwd || process.cwd(),
|
|
175
|
+
lastAssistantMessage: event.last_assistant_message || '',
|
|
176
|
+
cost: event.total_cost_usd || 0,
|
|
177
|
+
numTurns: event.num_turns || 0,
|
|
178
|
+
durationMs: event.duration_ms || 0,
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function writeErrorSignalFile (event) {
|
|
184
|
+
const sessionId = event.session_id || 'unknown';
|
|
185
|
+
writeSignalFile(`err_${sessionId}.json`, {
|
|
186
|
+
type: 'error',
|
|
187
|
+
cwd: event.cwd || process.cwd(),
|
|
188
|
+
error: event.error || 'unknown',
|
|
189
|
+
errorDetails: event.error_details || '',
|
|
190
|
+
lastAssistantMessage: event.last_assistant_message || '',
|
|
191
|
+
timestamp: Date.now(),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function writeReadySignalFile (event) {
|
|
196
|
+
const sessionId = event.session_id || 'unknown';
|
|
197
|
+
writeSignalFile(`rdy_${sessionId}.json`, {
|
|
198
|
+
type: 'ready',
|
|
199
|
+
cwd: event.cwd || process.cwd(),
|
|
200
|
+
model: event.model || '',
|
|
201
|
+
source: event.source || '',
|
|
202
|
+
timestamp: Date.now(),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function writeActivitySignalFile (event) {
|
|
207
|
+
const sessionId = event.session_id || 'unknown';
|
|
208
|
+
writeSignalFile(`act_${sessionId}.json`, {
|
|
209
|
+
type: 'activity',
|
|
210
|
+
cwd: event.cwd || process.cwd(),
|
|
211
|
+
toolName: event.tool_name || '',
|
|
212
|
+
toolInput: event.tool_input || {},
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function writeCompactSignalFile (event) {
|
|
218
|
+
const sessionId = event.session_id || 'unknown';
|
|
219
|
+
writeSignalFile(`cmp_${sessionId}.json`, {
|
|
220
|
+
type: 'compact',
|
|
221
|
+
cwd: event.cwd || process.cwd(),
|
|
222
|
+
summary: event.compact_summary || '',
|
|
223
|
+
trigger: event.trigger || '',
|
|
224
|
+
timestamp: Date.now(),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
181
228
|
// ----------------------
|
|
182
229
|
// STATE FILE
|
|
183
230
|
// ----------------------
|
|
@@ -659,10 +706,32 @@ process.stdin.on('end', async () => {
|
|
|
659
706
|
process.exit(0);
|
|
660
707
|
}
|
|
661
708
|
|
|
662
|
-
// For listener-only mode:
|
|
709
|
+
// For listener-only mode: handle events via signal files, then exit
|
|
663
710
|
if (disabled === 'listener-only') {
|
|
664
|
-
|
|
665
|
-
|
|
711
|
+
switch (eventType) {
|
|
712
|
+
case 'PermissionRequest':
|
|
713
|
+
process.stdout.write(JSON.stringify({
|
|
714
|
+
hookSpecificOutput: {
|
|
715
|
+
hookEventName: 'PermissionRequest',
|
|
716
|
+
decision: { behavior: 'allow' },
|
|
717
|
+
},
|
|
718
|
+
}));
|
|
719
|
+
break;
|
|
720
|
+
case 'Stop':
|
|
721
|
+
writePtySignalFile(event);
|
|
722
|
+
break;
|
|
723
|
+
case 'StopFailure':
|
|
724
|
+
writeErrorSignalFile(event);
|
|
725
|
+
break;
|
|
726
|
+
case 'SessionStart':
|
|
727
|
+
writeReadySignalFile(event);
|
|
728
|
+
break;
|
|
729
|
+
case 'PostToolUse':
|
|
730
|
+
writeActivitySignalFile(event);
|
|
731
|
+
break;
|
|
732
|
+
case 'PostCompact':
|
|
733
|
+
writeCompactSignalFile(event);
|
|
734
|
+
break;
|
|
666
735
|
}
|
|
667
736
|
process.exit(0);
|
|
668
737
|
}
|
|
@@ -691,7 +760,7 @@ process.stdin.on('end', async () => {
|
|
|
691
760
|
// STOP / NOTIFICATION EVENT
|
|
692
761
|
// ----------------------
|
|
693
762
|
|
|
694
|
-
if (eventType !== 'Stop' && eventType !== 'Notification') {
|
|
763
|
+
if (eventType !== 'Stop' && eventType !== 'Notification' && eventType !== 'StopFailure') {
|
|
695
764
|
process.exit(0);
|
|
696
765
|
}
|
|
697
766
|
|
|
@@ -709,8 +778,8 @@ process.stdin.on('end', async () => {
|
|
|
709
778
|
process.exit(0);
|
|
710
779
|
}
|
|
711
780
|
|
|
712
|
-
const statusEmoji = eventType === 'Notification' ? '⏸' : '✅';
|
|
713
|
-
const desktopStatus = eventType === 'Notification' ? 'Waiting' : 'Finished';
|
|
781
|
+
const statusEmoji = eventType === 'Notification' ? '⏸' : eventType === 'StopFailure' ? '❌' : '✅';
|
|
782
|
+
const desktopStatus = eventType === 'Notification' ? 'Waiting' : eventType === 'StopFailure' ? `Error: ${event.error || 'unknown'}` : 'Finished';
|
|
714
783
|
|
|
715
784
|
const branch = getBranch(cwd);
|
|
716
785
|
let label = `/${project}`;
|
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.1.
|
|
4
|
+
"version": "1.1.56",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|