claude-notification-plugin 1.1.65 → 1.1.74
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 +3 -0
- package/commit-sha +1 -1
- package/listener/LISTENER-DETAILED.md +86 -7
- package/listener/jsonl-reader.js +376 -0
- package/listener/listener.js +70 -19
- package/listener/pty-runner.js +13 -1
- package/notifier/notifier.js +72 -12
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.74",
|
|
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
|
@@ -134,6 +134,9 @@ ENV: `CLAUDE_NOTIFY_VOICE`
|
|
|
134
134
|
**notifyOnWaiting** — Notify when Claude is waiting for input. Default: **false**
|
|
135
135
|
ENV: `CLAUDE_NOTIFY_WAITING`
|
|
136
136
|
|
|
137
|
+
**notifyOnPermission** — Notify when Claude asks for tool permission (e.g. file edit confirmation). Default: **true**
|
|
138
|
+
ENV: `CLAUDE_NOTIFY_ON_PERMISSION`
|
|
139
|
+
|
|
137
140
|
**webhookUrl** — POST notification JSON to this URL. When set, all events (including user prompts) are sent. Set env to empty string (`""`) to disable per-project.
|
|
138
141
|
ENV: `CLAUDE_NOTIFY_WEBHOOK_URL`
|
|
139
142
|
|
package/commit-sha
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
40748d991517178165dc7d6f9c3a71b8b1ee7669
|
|
@@ -204,6 +204,7 @@ Running two listeners is impossible — the PID file prevents it. And this is im
|
|
|
204
204
|
| **WorktreeManager** | `worktree-manager.js` | Creates and removes git worktrees. Auto-discovery via `git worktree list`. Maps `&project/branch` to a path on disk |
|
|
205
205
|
| **Logger** | `logger.js` | Writes operational log to `~/.claude/.cc-n-listener.log`. Rotation when exceeding 5 MB (old file → `.log.old`) |
|
|
206
206
|
| **TaskLogger** | `task-logger.js` | Writes task Q&A logs (questions to Claude and answers). Separate file per project/branch. Rotation at 5 MB |
|
|
207
|
+
| **JsonlReader** | `jsonl-reader.js` | Reads Claude Code's structured JSONL session files incrementally. Provides clean, semantic live console content (text responses, tool calls) instead of raw PTY output |
|
|
207
208
|
|
|
208
209
|
---
|
|
209
210
|
|
|
@@ -762,14 +763,91 @@ Shows a brief reference for all commands.
|
|
|
762
763
|
|
|
763
764
|
### Live console
|
|
764
765
|
|
|
765
|
-
When **`liveConsole`** is enabled (default: `true`), the "⏳ Running..." message in Telegram is periodically updated with
|
|
766
|
+
When **`liveConsole`** is enabled (default: `true`), the "⏳ Running..." message in Telegram is periodically updated with what Claude is doing in real-time.
|
|
766
767
|
|
|
767
|
-
|
|
768
|
+
#### Data source: JSONL vs PTY
|
|
768
769
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
770
|
+
The live console can read data from two sources:
|
|
771
|
+
|
|
772
|
+
1. **JSONL session files** (default, preferred) — Claude Code writes structured JSONL files to `~/.claude/projects/{encodedCwd}/{sessionId}.jsonl`. Each line is a full JSON message with role, content (text, tool_use, thinking), model, timestamps, and cost. This gives clean, semantic output: the actual text Claude wrote and which tools it called.
|
|
773
|
+
|
|
774
|
+
2. **PTY buffer** (fallback) — raw terminal output from the pseudo-terminal, cleaned from ANSI escape codes and Claude Code UI chrome. This is a legacy approach that produces noisier output.
|
|
775
|
+
|
|
776
|
+
Controlled by the `liveConsoleSource` config parameter:
|
|
777
|
+
- `"auto"` (default) — try JSONL first, fall back to PTY buffer
|
|
778
|
+
- `"jsonl"` — use only JSONL (no output if file not found)
|
|
779
|
+
- `"pty"` — use only PTY buffer (legacy behavior)
|
|
780
|
+
|
|
781
|
+
#### How the JSONL path is resolved
|
|
782
|
+
|
|
783
|
+
The JSONL file path follows the convention:
|
|
784
|
+
```
|
|
785
|
+
~/.claude/projects/{encodedCwd}/{sessionId}.jsonl
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Step 1: Determine `sessionId`**
|
|
789
|
+
|
|
790
|
+
When Claude Code starts a session, its `SessionStart` hook writes a signal file `rdy_{sessionId}.json` to `~/.claude/pty-signals/`. The listener's PTY runner already polls this directory. On receiving a `ready` signal, the runner extracts `sessionId` from the filename and stores it in the session state.
|
|
791
|
+
|
|
792
|
+
**Step 2: Encode `cwd` into a project directory name**
|
|
793
|
+
|
|
794
|
+
Claude Code encodes the working directory path into a folder name by replacing special characters with dashes:
|
|
795
|
+
|
|
796
|
+
| Character | Replacement |
|
|
797
|
+
|---|---|
|
|
798
|
+
| `:` | `-` |
|
|
799
|
+
| `\` | `-` |
|
|
800
|
+
| `/` | `-` |
|
|
801
|
+
| `.` | `-` |
|
|
802
|
+
| `_` | `-` |
|
|
803
|
+
|
|
804
|
+
All other characters (letters, digits, existing `-`) are preserved as-is.
|
|
805
|
+
|
|
806
|
+
Examples:
|
|
807
|
+
|
|
808
|
+
| Working directory | Encoded project dir |
|
|
809
|
+
|---|---|
|
|
810
|
+
| `D:\DEV\FA\_pub\claude-notification-plugin` | `D--DEV-FA--pub-claude-notification-plugin` |
|
|
811
|
+
| `D:\DEV\FA\_cur\work-fast--testing` | `D--DEV-FA--cur-work-fast--testing` |
|
|
812
|
+
| `/home/user/projects/api-server` | `-home-user-projects-api-server` |
|
|
813
|
+
|
|
814
|
+
The double dashes (`--`) arise naturally from consecutive special characters, e.g. `\_` → `-` + `-` = `--`.
|
|
815
|
+
|
|
816
|
+
**Step 3: Construct the full path and open a reader**
|
|
817
|
+
|
|
818
|
+
```
|
|
819
|
+
sessionId = "08b1fa75-91fe-4c1e-a47e-db9b99af7fb5"
|
|
820
|
+
cwd = "D:\DEV\FA\_pub\claude-notification-plugin"
|
|
821
|
+
↓ encode
|
|
822
|
+
encodedCwd = "D--DEV-FA--pub-claude-notification-plugin"
|
|
823
|
+
↓ join
|
|
824
|
+
path = ~/.claude/projects/D--DEV-FA--pub-claude-notification-plugin/08b1fa75-91fe-4c1e-a47e-db9b99af7fb5.jsonl
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
If `sessionId` is not yet known (session hasn't started), the fallback picks the most recently modified `.jsonl` file in the project directory (within the last 2 minutes).
|
|
828
|
+
|
|
829
|
+
#### What is displayed
|
|
830
|
+
|
|
831
|
+
From the JSONL data, the live console extracts:
|
|
832
|
+
- **Text responses** — the actual text Claude wrote (from `content[].type === "text"`)
|
|
833
|
+
- **Tool calls** — formatted as `🔧 Read: config.json`, `🔧 $ npm test`, `🔧 Grep: pattern`, etc. (from `content[].type === "tool_use"`)
|
|
834
|
+
- **Thinking blocks** are skipped (not shown)
|
|
835
|
+
|
|
836
|
+
This is far more informative than raw PTY output: you see the real tool calls and actual responses, not garbled terminal fragments.
|
|
837
|
+
|
|
838
|
+
#### Incremental reading
|
|
839
|
+
|
|
840
|
+
The JSONL reader tracks its byte offset in the file and only reads new data on each poll. Since JSONL files are append-only, this is safe and efficient — no re-reading of the entire file.
|
|
841
|
+
|
|
842
|
+
#### Configuration
|
|
843
|
+
|
|
844
|
+
| Parameter | Default | Description |
|
|
845
|
+
|---|---|---|
|
|
846
|
+
| `liveConsole` | `true` | Enable/disable live console |
|
|
847
|
+
| `liveConsoleIntervalMillis` | `1000` | Update interval in milliseconds |
|
|
848
|
+
| `liveConsoleMaxOutputChars` | `300` | Max chars of PTY output per update |
|
|
849
|
+
| `liveConsoleSource` | `"auto"` | Data source: `"auto"`, `"jsonl"`, or `"pty"` |
|
|
850
|
+
| `jsonlMaxContentChars` | `500` | Max chars of JSONL content per update |
|
|
773
851
|
|
|
774
852
|
### PTY logs
|
|
775
853
|
|
|
@@ -792,8 +870,9 @@ Send `/pty` or `/pty &project` in Telegram to get instant diagnostics:
|
|
|
792
870
|
- Buffer size in bytes
|
|
793
871
|
- Elapsed time since task start
|
|
794
872
|
- Whether live console interval is active
|
|
873
|
+
- Whether JSONL source is active
|
|
795
874
|
- Whether PTY log stream is writing
|
|
796
|
-
- Last 15 lines of
|
|
875
|
+
- Last 15 lines of output (from JSONL if available, otherwise from PTY buffer)
|
|
797
876
|
|
|
798
877
|
---
|
|
799
878
|
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CLAUDE_DIR } from '../bin/constants.js';
|
|
4
|
+
|
|
5
|
+
const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Encode a cwd path into the Claude Code project directory name.
|
|
9
|
+
* Replaces : \ / . _ with dashes.
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* D:\DEV\FA\_pub\my-project → D--DEV-FA--pub-my-project
|
|
13
|
+
* /home/user/projects/api → -home-user-projects-api
|
|
14
|
+
*/
|
|
15
|
+
export function cwdToProjectDir (cwd) {
|
|
16
|
+
return cwd.replace(/[:\\/._]/g, '-');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Find the JSONL file path for a given cwd and sessionId.
|
|
21
|
+
* Returns null if the file does not exist.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveJsonlPath (cwd, sessionId) {
|
|
24
|
+
if (!sessionId) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const dirName = cwdToProjectDir(cwd);
|
|
28
|
+
const filePath = path.join(PROJECTS_DIR, dirName, `${sessionId}.jsonl`);
|
|
29
|
+
try {
|
|
30
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
31
|
+
return filePath;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find the most recently modified JSONL file for a given cwd.
|
|
39
|
+
* Used as a fallback when sessionId is not yet known.
|
|
40
|
+
* Returns null if no fresh file is found within maxAgeMs.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveJsonlByMtime (cwd, maxAgeMs = 120_000) {
|
|
43
|
+
const dirName = cwdToProjectDir(cwd);
|
|
44
|
+
const dirPath = path.join(PROJECTS_DIR, dirName);
|
|
45
|
+
let files;
|
|
46
|
+
try {
|
|
47
|
+
files = fs.readdirSync(dirPath);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let best = null;
|
|
53
|
+
let bestMtime = 0;
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
|
|
56
|
+
for (const f of files) {
|
|
57
|
+
if (!f.endsWith('.jsonl')) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const stat = fs.statSync(path.join(dirPath, f));
|
|
62
|
+
if (stat.mtimeMs > bestMtime && now - stat.mtimeMs < maxAgeMs) {
|
|
63
|
+
bestMtime = stat.mtimeMs;
|
|
64
|
+
best = path.join(dirPath, f);
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return best;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Incremental JSONL file reader.
|
|
76
|
+
* Reads new lines from a JSONL file since the last read.
|
|
77
|
+
*/
|
|
78
|
+
export class JsonlReader {
|
|
79
|
+
constructor (filePath, logger) {
|
|
80
|
+
this.filePath = filePath;
|
|
81
|
+
this.logger = logger || null;
|
|
82
|
+
this._offset = 0;
|
|
83
|
+
this._remainder = '';
|
|
84
|
+
this._lastAssistantText = '';
|
|
85
|
+
this._lastToolUse = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Read new lines since last call, parse JSON, update internal state.
|
|
90
|
+
* Returns array of parsed JSONL objects (only new ones).
|
|
91
|
+
*/
|
|
92
|
+
readNew () {
|
|
93
|
+
let fd;
|
|
94
|
+
try {
|
|
95
|
+
fd = fs.openSync(this.filePath, 'r');
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const stat = fs.fstatSync(fd);
|
|
102
|
+
if (stat.size <= this._offset) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const buf = Buffer.alloc(stat.size - this._offset);
|
|
107
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, this._offset);
|
|
108
|
+
if (bytesRead === 0) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
this._offset += bytesRead;
|
|
112
|
+
|
|
113
|
+
const chunk = this._remainder + buf.toString('utf-8', 0, bytesRead);
|
|
114
|
+
const lines = chunk.split('\n');
|
|
115
|
+
// Last element may be incomplete — save as remainder
|
|
116
|
+
this._remainder = lines.pop() || '';
|
|
117
|
+
|
|
118
|
+
const entries = [];
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
const trimmed = line.trim();
|
|
121
|
+
if (!trimmed) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const obj = JSON.parse(trimmed);
|
|
126
|
+
entries.push(obj);
|
|
127
|
+
this._processEntry(obj);
|
|
128
|
+
} catch {
|
|
129
|
+
// skip malformed lines
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return entries;
|
|
133
|
+
} finally {
|
|
134
|
+
fs.closeSync(fd);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Process a parsed JSONL entry to update last assistant content.
|
|
140
|
+
*/
|
|
141
|
+
_processEntry (entry) {
|
|
142
|
+
if (entry.message?.role !== 'assistant') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const content = entry.message?.content;
|
|
147
|
+
if (!Array.isArray(content)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const block of content) {
|
|
152
|
+
if (block.type === 'text' && block.text) {
|
|
153
|
+
this._lastAssistantText = block.text;
|
|
154
|
+
this._lastToolUse = null; // text supersedes tool_use display
|
|
155
|
+
} else if (block.type === 'tool_use') {
|
|
156
|
+
this._lastToolUse = {
|
|
157
|
+
name: block.name || 'unknown',
|
|
158
|
+
input: block.input || {},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// thinking blocks are ignored for display
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get formatted display text from the last assistant message.
|
|
167
|
+
* Returns a short summary suitable for Telegram live console.
|
|
168
|
+
*/
|
|
169
|
+
getDisplayContent (maxChars = 300) {
|
|
170
|
+
const parts = [];
|
|
171
|
+
|
|
172
|
+
if (this._lastToolUse) {
|
|
173
|
+
parts.push(formatToolUse(this._lastToolUse));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (this._lastAssistantText) {
|
|
177
|
+
parts.push(this._lastAssistantText);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (parts.length === 0) {
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let result = parts.join('\n\n');
|
|
185
|
+
if (result.length > maxChars) {
|
|
186
|
+
result = result.slice(-maxChars);
|
|
187
|
+
// Trim to last complete line
|
|
188
|
+
const nlIdx = result.indexOf('\n');
|
|
189
|
+
if (nlIdx > 0) {
|
|
190
|
+
result = result.slice(nlIdx + 1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Reset reader to re-read from the beginning.
|
|
198
|
+
*/
|
|
199
|
+
reset () {
|
|
200
|
+
this._offset = 0;
|
|
201
|
+
this._remainder = '';
|
|
202
|
+
this._lastAssistantText = '';
|
|
203
|
+
this._lastToolUse = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Format a tool_use block into a short display string.
|
|
209
|
+
*/
|
|
210
|
+
function formatToolUse (tool) {
|
|
211
|
+
const name = tool.name || '';
|
|
212
|
+
const input = tool.input || {};
|
|
213
|
+
const trunc = (s, n) => (typeof s === 'string' && s.length > n ? s.slice(0, n - 1) + '…' : s);
|
|
214
|
+
switch (name) {
|
|
215
|
+
case 'Read':
|
|
216
|
+
case 'Write':
|
|
217
|
+
case 'Edit':
|
|
218
|
+
return input.file_path
|
|
219
|
+
? `🔧 ${name}: ${path.basename(input.file_path)}`
|
|
220
|
+
: `🔧 ${name}`;
|
|
221
|
+
case 'Bash':
|
|
222
|
+
return input.command
|
|
223
|
+
? `🔧 $ ${trunc(input.command, 80)}${input.run_in_background ? ' (bg)' : ''}${input.timeout ? ` (timeout ${input.timeout})` : ''}`
|
|
224
|
+
: '🔧 Bash';
|
|
225
|
+
case 'Grep':
|
|
226
|
+
if (input.pattern) {
|
|
227
|
+
const where = typeof input.path === 'string'
|
|
228
|
+
? path.basename(input.path)
|
|
229
|
+
: (typeof input.glob === 'string' ? input.glob : '');
|
|
230
|
+
|
|
231
|
+
const flags = [];
|
|
232
|
+
if (input['-n']) flags.push('-n');
|
|
233
|
+
if (input['-C']) flags.push(`-C ${input['-C']}`);
|
|
234
|
+
if (!input['-C'] && (typeof input.context === 'number' || typeof input.context === 'string')) {
|
|
235
|
+
flags.push(`-C ${input.context}`);
|
|
236
|
+
}
|
|
237
|
+
if (input['-i']) flags.push('-i');
|
|
238
|
+
if (input['-A']) flags.push(`-A ${input['-A']}`);
|
|
239
|
+
if (input['-B']) flags.push(`-B ${input['-B']}`);
|
|
240
|
+
if (input.head_limit) flags.push(`head ${input.head_limit}`);
|
|
241
|
+
const flagStr = flags.length ? ` ${flags.join(' ')}` : '';
|
|
242
|
+
|
|
243
|
+
return where
|
|
244
|
+
? `🔧 Grep${flagStr}: ${trunc(input.pattern, 60)} in ${trunc(where, 30)}`
|
|
245
|
+
: `🔧 Grep${flagStr}: ${trunc(input.pattern, 80)}`;
|
|
246
|
+
}
|
|
247
|
+
return '🔧 Grep';
|
|
248
|
+
case 'Glob':
|
|
249
|
+
if (input.pattern) {
|
|
250
|
+
const p = typeof input.path === 'string' ? path.basename(input.path) : '';
|
|
251
|
+
return p ? `🔧 Glob: ${trunc(input.pattern, 60)} in ${trunc(p, 30)}` : `🔧 Glob: ${trunc(input.pattern, 80)}`;
|
|
252
|
+
}
|
|
253
|
+
return '🔧 Glob';
|
|
254
|
+
case 'Agent':
|
|
255
|
+
if (input.description) {
|
|
256
|
+
const bg = input.run_in_background ? ' (bg)' : '';
|
|
257
|
+
const st = typeof input.subagent_type === 'string' && input.subagent_type.trim()
|
|
258
|
+
? ` [${input.subagent_type.trim()}]`
|
|
259
|
+
: '';
|
|
260
|
+
return `🔧 Agent${bg}${st}: ${trunc(input.description, 80)}`;
|
|
261
|
+
}
|
|
262
|
+
return '🔧 Agent';
|
|
263
|
+
case 'Skill':
|
|
264
|
+
return input.skill
|
|
265
|
+
? `🔧 Skill: ${input.skill}${input.args ? ` ${trunc(String(input.args), 80)}` : ''}`
|
|
266
|
+
: '🔧 Skill';
|
|
267
|
+
case 'WebFetch':
|
|
268
|
+
if (input.url) {
|
|
269
|
+
const hasPrompt = typeof input.prompt === 'string' && input.prompt.trim();
|
|
270
|
+
return `🔧 Fetch${hasPrompt ? '*' : ''}: ${trunc(input.url, 80)}`;
|
|
271
|
+
}
|
|
272
|
+
return '🔧 WebFetch';
|
|
273
|
+
case 'WebSearch':
|
|
274
|
+
return input.query ? `🔧 Search: ${input.query}` : '🔧 WebSearch';
|
|
275
|
+
case 'ToolSearch':
|
|
276
|
+
return input.query ? `🔧 ToolSearch: ${trunc(input.query, 200)}` : '🔧 ToolSearch';
|
|
277
|
+
case 'TaskCreate':
|
|
278
|
+
return input.subject
|
|
279
|
+
? `🔧 Task+: ${trunc(input.subject, 100)}`
|
|
280
|
+
: '🔧 TaskCreate';
|
|
281
|
+
case 'TaskUpdate':
|
|
282
|
+
return input.taskId && input.status
|
|
283
|
+
? `🔧 Task#${input.taskId}: ${input.status}`
|
|
284
|
+
: (input.taskId ? `🔧 Task#${input.taskId}` : '🔧 TaskUpdate');
|
|
285
|
+
case 'ExitPlanMode':
|
|
286
|
+
return input.planFilePath
|
|
287
|
+
? `🔧 ExitPlanMode: ${path.basename(input.planFilePath)}`
|
|
288
|
+
: '🔧 ExitPlanMode';
|
|
289
|
+
case 'Task':
|
|
290
|
+
if (input.description) {
|
|
291
|
+
const st = typeof input.subagent_type === 'string' && input.subagent_type.trim()
|
|
292
|
+
? ` [${input.subagent_type.trim()}]`
|
|
293
|
+
: '';
|
|
294
|
+
return `🔧 Task${st}: ${trunc(input.description, 80)}`;
|
|
295
|
+
}
|
|
296
|
+
return '🔧 Task';
|
|
297
|
+
case 'TaskOutput':
|
|
298
|
+
if (input.task_id) {
|
|
299
|
+
return `🔧 Task#${input.task_id}: output${input.timeout ? ` (timeout ${input.timeout})` : ''}`;
|
|
300
|
+
}
|
|
301
|
+
return '🔧 TaskOutput';
|
|
302
|
+
case 'AskUserQuestion': {
|
|
303
|
+
const qs = Array.isArray(input.questions) ? input.questions : [];
|
|
304
|
+
if (qs.length > 0) {
|
|
305
|
+
const first = qs[0] || {};
|
|
306
|
+
const head = (typeof first.header === 'string' && first.header.trim())
|
|
307
|
+
? first.header.trim()
|
|
308
|
+
: (typeof first.question === 'string' ? first.question.trim() : '');
|
|
309
|
+
const suffix = qs.length > 1 ? ` (+${qs.length - 1})` : '';
|
|
310
|
+
if (head) {
|
|
311
|
+
return `🔧 Ask: ${trunc(head, 120)}${suffix}`;
|
|
312
|
+
}
|
|
313
|
+
return `🔧 AskUserQuestion${suffix}`;
|
|
314
|
+
}
|
|
315
|
+
return '🔧 AskUserQuestion';
|
|
316
|
+
}
|
|
317
|
+
default:
|
|
318
|
+
if (name.startsWith('mcp__playwright__browser_')) {
|
|
319
|
+
const action = name.slice('mcp__playwright__browser_'.length);
|
|
320
|
+
switch (action) {
|
|
321
|
+
case 'navigate':
|
|
322
|
+
return input.url ? `🔧 PW nav: ${trunc(input.url, 80)}` : '🔧 PW nav';
|
|
323
|
+
case 'click': {
|
|
324
|
+
const ref = typeof input.ref === 'string' ? input.ref : '';
|
|
325
|
+
const el = typeof input.element === 'string' ? input.element : '';
|
|
326
|
+
return `🔧 PW click: ${trunc(ref || el || '', 80)}`.trim();
|
|
327
|
+
}
|
|
328
|
+
case 'type': {
|
|
329
|
+
const ref = typeof input.ref === 'string' ? input.ref : '';
|
|
330
|
+
const text = typeof input.text === 'string' ? input.text : '';
|
|
331
|
+
return `🔧 PW type: ${trunc(ref || '', 40)}${text ? ` (${text.length} chars)` : ''}`.trim();
|
|
332
|
+
}
|
|
333
|
+
case 'take_screenshot':
|
|
334
|
+
return input.filename ? `🔧 PW shot: ${trunc(input.filename, 80)}` : '🔧 PW shot';
|
|
335
|
+
case 'wait_for':
|
|
336
|
+
return input.time ? `🔧 PW wait: ${input.time}` : '🔧 PW wait';
|
|
337
|
+
case 'console_messages':
|
|
338
|
+
return input.level ? `🔧 PW console: ${input.level}` : '🔧 PW console';
|
|
339
|
+
case 'snapshot':
|
|
340
|
+
return '🔧 PW snapshot';
|
|
341
|
+
default:
|
|
342
|
+
return `🔧 PW: ${action}`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (name === 'mcp__sequential-thinking__sequentialthinking') {
|
|
346
|
+
const n = input.thoughtNumber;
|
|
347
|
+
const total = input.totalThoughts;
|
|
348
|
+
const head = (typeof n === 'number' || typeof n === 'string') && (typeof total === 'number' || typeof total === 'string')
|
|
349
|
+
? `${n}/${total} `
|
|
350
|
+
: '';
|
|
351
|
+
const thought = typeof input.thought === 'string' ? trunc(input.thought.trim(), 80) : '';
|
|
352
|
+
return `🔧 Think: ${head}${thought}`.trim();
|
|
353
|
+
}
|
|
354
|
+
if (name === 'mcp__serena__list_dir') {
|
|
355
|
+
return input.relative_path ? `🔧 Serena ls: ${trunc(input.relative_path, 80)}` : '🔧 Serena ls';
|
|
356
|
+
}
|
|
357
|
+
if (name === 'mcp__serena__get_symbols_overview') {
|
|
358
|
+
return input.relative_path ? `🔧 Serena symbols: ${trunc(input.relative_path, 80)}` : '🔧 Serena symbols';
|
|
359
|
+
}
|
|
360
|
+
if (name.startsWith('mcp__')) {
|
|
361
|
+
if (name === 'mcp__context7__resolve-library-id') {
|
|
362
|
+
return input.libraryName
|
|
363
|
+
? `🔧 C7 lib: ${trunc(input.libraryName, 60)}`
|
|
364
|
+
: '🔧 C7 resolve';
|
|
365
|
+
}
|
|
366
|
+
if (name === 'mcp__context7__query-docs') {
|
|
367
|
+
const lib = typeof input.libraryId === 'string' ? input.libraryId : '';
|
|
368
|
+
const q = typeof input.query === 'string' ? input.query : '';
|
|
369
|
+
return `🔧 C7 docs: ${trunc(q || lib || '', 80)}`.trim();
|
|
370
|
+
}
|
|
371
|
+
const parts = name.split('__');
|
|
372
|
+
return parts.length >= 3 ? `🔧 MCP ${parts[1]}: ${parts[2]}` : `🔧 ${name}`;
|
|
373
|
+
}
|
|
374
|
+
return `🔧 ${name}`;
|
|
375
|
+
}
|
|
376
|
+
}
|
package/listener/listener.js
CHANGED
|
@@ -12,6 +12,7 @@ import { PtyRunner } from './pty-runner.js';
|
|
|
12
12
|
import { WorktreeManager } from './worktree-manager.js';
|
|
13
13
|
import { parseMessage, parseTarget } from './message-parser.js';
|
|
14
14
|
import { CLAUDE_DIR, CONFIG_PATH, LISTENER_LOG_FILENAME } from '../bin/constants.js';
|
|
15
|
+
import { JsonlReader, resolveJsonlPath, resolveJsonlByMtime } from './jsonl-reader.js';
|
|
15
16
|
|
|
16
17
|
// ----------------------
|
|
17
18
|
// CRASH PROTECTION
|
|
@@ -138,6 +139,11 @@ const sessions = new Map();
|
|
|
138
139
|
const freshSessionDirs = new Set();
|
|
139
140
|
// Live console intervals per workDir
|
|
140
141
|
const liveConsoleTimers = new Map();
|
|
142
|
+
// JSONL readers per workDir (for live console from structured session data)
|
|
143
|
+
const jsonlReaders = new Map();
|
|
144
|
+
// Live console source: "jsonl" | "pty" | "auto" (default: "auto")
|
|
145
|
+
const liveConsoleSource = listenerConfig.liveConsoleSource || 'auto';
|
|
146
|
+
const jsonlMaxContentChars = listenerConfig.jsonlMaxContentChars || 500;
|
|
141
147
|
|
|
142
148
|
logger.info('Listener started');
|
|
143
149
|
logger.info(`Projects: ${JSON.stringify(Object.keys(listenerConfig.projects))}`);
|
|
@@ -322,6 +328,49 @@ function shouldContinueSession (workDir) {
|
|
|
322
328
|
return sessions.has(workDir);
|
|
323
329
|
}
|
|
324
330
|
|
|
331
|
+
function _initJsonlReader (workDir) {
|
|
332
|
+
const sessionId = runner.getSessionId(workDir);
|
|
333
|
+
const jsonlPath = sessionId
|
|
334
|
+
? resolveJsonlPath(workDir, sessionId)
|
|
335
|
+
: resolveJsonlByMtime(workDir);
|
|
336
|
+
if (jsonlPath) {
|
|
337
|
+
const reader = new JsonlReader(jsonlPath, logger);
|
|
338
|
+
jsonlReaders.set(workDir, reader);
|
|
339
|
+
logger.info(`JSONL reader initialized: ${jsonlPath}`);
|
|
340
|
+
return reader;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function _getJsonlContent (workDir) {
|
|
346
|
+
let reader = jsonlReaders.get(workDir);
|
|
347
|
+
if (!reader) {
|
|
348
|
+
reader = _initJsonlReader(workDir);
|
|
349
|
+
}
|
|
350
|
+
if (!reader) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
reader.readNew();
|
|
354
|
+
return reader.getDisplayContent(jsonlMaxContentChars);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function _getPtyContent (workDir) {
|
|
358
|
+
const raw = runner.getBuffer(workDir);
|
|
359
|
+
if (!raw) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
const cleaned = cleanPtyOutput(raw);
|
|
363
|
+
if (!cleaned) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const tail = cleaned.length > liveConsoleMaxOutputChars
|
|
367
|
+
? cleaned.slice(-liveConsoleMaxOutputChars)
|
|
368
|
+
: cleaned;
|
|
369
|
+
return cleaned.length > liveConsoleMaxOutputChars
|
|
370
|
+
? tail.slice(tail.indexOf('\n') + 1)
|
|
371
|
+
: tail;
|
|
372
|
+
}
|
|
373
|
+
|
|
325
374
|
function startLiveConsole (workDir, messageId, header) {
|
|
326
375
|
stopLiveConsole(workDir);
|
|
327
376
|
if (!liveConsoleEnabled || !messageId) {
|
|
@@ -330,22 +379,13 @@ function startLiveConsole (workDir, messageId, header) {
|
|
|
330
379
|
let lastSentText = '';
|
|
331
380
|
const timer = setInterval(async () => {
|
|
332
381
|
try {
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
382
|
+
let output = null;
|
|
383
|
+
if (liveConsoleSource === 'jsonl' || liveConsoleSource === 'auto') {
|
|
384
|
+
output = _getJsonlContent(workDir);
|
|
336
385
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return;
|
|
386
|
+
if (!output && (liveConsoleSource === 'pty' || liveConsoleSource === 'auto')) {
|
|
387
|
+
output = _getPtyContent(workDir);
|
|
340
388
|
}
|
|
341
|
-
// Take the tail that fits
|
|
342
|
-
const tail = cleaned.length > liveConsoleMaxOutputChars
|
|
343
|
-
? cleaned.slice(-liveConsoleMaxOutputChars)
|
|
344
|
-
: cleaned;
|
|
345
|
-
// Trim to last complete line if we sliced mid-line
|
|
346
|
-
const output = cleaned.length > liveConsoleMaxOutputChars
|
|
347
|
-
? tail.slice(tail.indexOf('\n') + 1)
|
|
348
|
-
: tail;
|
|
349
389
|
if (!output || output === lastSentText) {
|
|
350
390
|
return;
|
|
351
391
|
}
|
|
@@ -370,6 +410,7 @@ function stopLiveConsole (workDir) {
|
|
|
370
410
|
clearInterval(timer);
|
|
371
411
|
liveConsoleTimers.delete(workDir);
|
|
372
412
|
}
|
|
413
|
+
jsonlReaders.delete(workDir);
|
|
373
414
|
}
|
|
374
415
|
|
|
375
416
|
async function startTask (workDir, task) {
|
|
@@ -830,17 +871,27 @@ function formatPtyInfo (project, branch, workDir, info) {
|
|
|
830
871
|
? formatDuration(Date.now() - new Date(info.startedAt).getTime())
|
|
831
872
|
: '-';
|
|
832
873
|
const liveTimer = liveConsoleTimers.has(workDir) ? '✅' : '❌';
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
874
|
+
const hasJsonl = jsonlReaders.has(workDir) ? '✅' : '❌';
|
|
875
|
+
|
|
876
|
+
// Prefer JSONL content if available, fall back to PTY buffer
|
|
877
|
+
let lastLines = '(empty)';
|
|
878
|
+
const jsonlContent = _getJsonlContent(workDir);
|
|
879
|
+
if (jsonlContent) {
|
|
880
|
+
lastLines = jsonlContent.split('\n').slice(-15).join('\n');
|
|
881
|
+
} else {
|
|
882
|
+
const raw = runner.getBuffer(workDir);
|
|
883
|
+
const cleaned = raw ? cleanPtyOutput(raw) : '';
|
|
884
|
+
if (cleaned) {
|
|
885
|
+
lastLines = cleaned.split('\n').slice(-15).join('\n');
|
|
886
|
+
}
|
|
887
|
+
}
|
|
838
888
|
|
|
839
889
|
return `<b>${escapeHtml(label)}</b>
|
|
840
890
|
State: <code>${info.state}</code>
|
|
841
891
|
Buffer: <code>${info.bufferSize}</code> bytes
|
|
842
892
|
Elapsed: ${elapsed}
|
|
843
893
|
Live console: ${liveTimer}
|
|
894
|
+
JSONL source: ${hasJsonl}
|
|
844
895
|
PTY log: <code>${info.hasLogStream ? 'writing' : 'off'}</code>
|
|
845
896
|
|
|
846
897
|
<pre>${escapeHtml(lastLines)}</pre>`;
|
package/listener/pty-runner.js
CHANGED
|
@@ -135,12 +135,16 @@ export class PtyRunner extends EventEmitter {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
} else if (type === 'ready') {
|
|
138
|
-
// SessionStart — emit ready event
|
|
138
|
+
// SessionStart — emit ready event, capture sessionId from filename
|
|
139
139
|
this._unlinkSafe(filePath);
|
|
140
|
+
const signalSessionId = f.startsWith('rdy_') ? f.slice(4, -5) : null;
|
|
140
141
|
for (const [workDir, session] of this.sessions) {
|
|
141
142
|
if (this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
|
|
142
143
|
session._lastActivityTime = Date.now();
|
|
143
144
|
session._model = marker.model || '';
|
|
145
|
+
if (signalSessionId && signalSessionId !== 'unknown') {
|
|
146
|
+
session.sessionId = signalSessionId;
|
|
147
|
+
}
|
|
144
148
|
this.emit('ready', workDir, marker);
|
|
145
149
|
break;
|
|
146
150
|
}
|
|
@@ -613,6 +617,14 @@ export class PtyRunner extends EventEmitter {
|
|
|
613
617
|
return session?._buffer || '';
|
|
614
618
|
}
|
|
615
619
|
|
|
620
|
+
/**
|
|
621
|
+
* Get Claude session ID for a workDir (captured from SessionStart hook signal).
|
|
622
|
+
*/
|
|
623
|
+
getSessionId (workDir) {
|
|
624
|
+
const session = this.sessions.get(workDir);
|
|
625
|
+
return session?.sessionId || null;
|
|
626
|
+
}
|
|
627
|
+
|
|
616
628
|
/**
|
|
617
629
|
* Get last tool activity for a workDir (from PostToolUse hook signals).
|
|
618
630
|
*/
|
package/notifier/notifier.js
CHANGED
|
@@ -63,6 +63,7 @@ function loadConfig () {
|
|
|
63
63
|
webhookUrl: '',
|
|
64
64
|
notifyAfterSeconds: 15,
|
|
65
65
|
notifyOnWaiting: false,
|
|
66
|
+
notifyOnPermission: true,
|
|
66
67
|
debug: false,
|
|
67
68
|
};
|
|
68
69
|
|
|
@@ -88,6 +89,9 @@ function loadConfig () {
|
|
|
88
89
|
if (typeof user.notifyOnWaiting === 'boolean') {
|
|
89
90
|
config.notifyOnWaiting = user.notifyOnWaiting;
|
|
90
91
|
}
|
|
92
|
+
if (typeof user.notifyOnPermission === 'boolean') {
|
|
93
|
+
config.notifyOnPermission = user.notifyOnPermission;
|
|
94
|
+
}
|
|
91
95
|
if (typeof user.debug === 'boolean') {
|
|
92
96
|
config.debug = user.debug;
|
|
93
97
|
}
|
|
@@ -122,6 +126,9 @@ function loadConfig () {
|
|
|
122
126
|
if (process.env.CLAUDE_NOTIFY_WAITING !== undefined) {
|
|
123
127
|
config.notifyOnWaiting = process.env.CLAUDE_NOTIFY_WAITING === '1';
|
|
124
128
|
}
|
|
129
|
+
if (process.env.CLAUDE_NOTIFY_ON_PERMISSION !== undefined) {
|
|
130
|
+
config.notifyOnPermission = process.env.CLAUDE_NOTIFY_ON_PERMISSION === '1';
|
|
131
|
+
}
|
|
125
132
|
if (process.env.CLAUDE_NOTIFY_DEBUG !== undefined) {
|
|
126
133
|
config.debug = process.env.CLAUDE_NOTIFY_DEBUG === '1';
|
|
127
134
|
}
|
|
@@ -641,11 +648,10 @@ function getVoicePhrase (duration, project) {
|
|
|
641
648
|
return fn(duration, project || 'unknown');
|
|
642
649
|
}
|
|
643
650
|
|
|
644
|
-
function
|
|
651
|
+
function speakText (config, text) {
|
|
645
652
|
if (!config.voice.enabled) {
|
|
646
653
|
return;
|
|
647
654
|
}
|
|
648
|
-
const text = getVoicePhrase(duration, project);
|
|
649
655
|
try {
|
|
650
656
|
switch (PLATFORM) {
|
|
651
657
|
case 'win32': {
|
|
@@ -672,10 +678,26 @@ function speakResult (config, duration, project) {
|
|
|
672
678
|
}
|
|
673
679
|
}
|
|
674
680
|
} catch (err) {
|
|
675
|
-
debugLog(config, '
|
|
681
|
+
debugLog(config, 'speakText failed:', err.message);
|
|
676
682
|
}
|
|
677
683
|
}
|
|
678
684
|
|
|
685
|
+
function speakResult (config, duration, project) {
|
|
686
|
+
speakText(config, getVoicePhrase(duration, project));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const permissionVoicePhrases = {
|
|
690
|
+
en: (p, tool) => `Claude needs your permission on ${p} for ${tool}`,
|
|
691
|
+
ru: (p, tool) => `Клод ожидает разрешение в проекте ${p} на ${tool}`,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
function getPermissionVoicePhrase (project, toolName) {
|
|
695
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en';
|
|
696
|
+
const lang = locale.split('-')[0].toLowerCase();
|
|
697
|
+
const fn = permissionVoicePhrases[lang] || permissionVoicePhrases.en;
|
|
698
|
+
return fn(project || 'unknown', toolName || 'unknown');
|
|
699
|
+
}
|
|
700
|
+
|
|
679
701
|
// ----------------------
|
|
680
702
|
// READ HOOK INPUT
|
|
681
703
|
// ----------------------
|
|
@@ -760,7 +782,7 @@ process.stdin.on('end', async () => {
|
|
|
760
782
|
// STOP / NOTIFICATION EVENT
|
|
761
783
|
// ----------------------
|
|
762
784
|
|
|
763
|
-
if (eventType !== 'Stop' && eventType !== 'Notification' && eventType !== 'StopFailure') {
|
|
785
|
+
if (eventType !== 'Stop' && eventType !== 'Notification' && eventType !== 'StopFailure' && eventType !== 'PermissionRequest') {
|
|
764
786
|
process.exit(0);
|
|
765
787
|
}
|
|
766
788
|
|
|
@@ -768,6 +790,10 @@ process.stdin.on('end', async () => {
|
|
|
768
790
|
process.exit(0);
|
|
769
791
|
}
|
|
770
792
|
|
|
793
|
+
if (eventType === 'PermissionRequest' && !config.notifyOnPermission) {
|
|
794
|
+
process.exit(0);
|
|
795
|
+
}
|
|
796
|
+
|
|
771
797
|
let duration = 0;
|
|
772
798
|
const session = state.sessions[sessionId];
|
|
773
799
|
if (session?.start) {
|
|
@@ -778,8 +804,21 @@ process.stdin.on('end', async () => {
|
|
|
778
804
|
process.exit(0);
|
|
779
805
|
}
|
|
780
806
|
|
|
781
|
-
const
|
|
782
|
-
const
|
|
807
|
+
const permToolName = event.tool_name || 'unknown';
|
|
808
|
+
const permDetail = event.tool_input?.file_path || event.tool_input?.command?.slice(0, 80) || '';
|
|
809
|
+
|
|
810
|
+
const statusEmoji = eventType === 'PermissionRequest' ? '🔐' : eventType === 'Notification' ? '⏸' : eventType === 'StopFailure' ? '❌' : '✅';
|
|
811
|
+
|
|
812
|
+
let desktopStatus;
|
|
813
|
+
if (eventType === 'PermissionRequest') {
|
|
814
|
+
desktopStatus = `Permission: ${permToolName}${permDetail ? ` — ${path.basename(permDetail)}` : ''}`;
|
|
815
|
+
} else if (eventType === 'Notification') {
|
|
816
|
+
desktopStatus = 'Waiting';
|
|
817
|
+
} else if (eventType === 'StopFailure') {
|
|
818
|
+
desktopStatus = `Error: ${event.error || 'unknown'}`;
|
|
819
|
+
} else {
|
|
820
|
+
desktopStatus = 'Finished';
|
|
821
|
+
}
|
|
783
822
|
|
|
784
823
|
const branch = getBranch(cwd);
|
|
785
824
|
let label = `/${project}`;
|
|
@@ -794,8 +833,16 @@ process.stdin.on('end', async () => {
|
|
|
794
833
|
const desktopTitle = label;
|
|
795
834
|
const desktopMessage = desktopStatus;
|
|
796
835
|
|
|
797
|
-
let telegramMessage
|
|
798
|
-
|
|
836
|
+
let telegramMessage;
|
|
837
|
+
if (eventType === 'PermissionRequest') {
|
|
838
|
+
telegramMessage = `${statusEmoji} ${labelHtml}\nPermission: <b>${escapeHtml(permToolName)}</b>`;
|
|
839
|
+
if (permDetail) {
|
|
840
|
+
telegramMessage += `\n<code>${escapeHtml(permDetail)}</code>`;
|
|
841
|
+
}
|
|
842
|
+
telegramMessage += `\n(duration: ${duration}s)${triggerLine}`;
|
|
843
|
+
} else {
|
|
844
|
+
telegramMessage = `${statusEmoji} ${labelHtml}\n(duration: ${duration}s)${triggerLine}`;
|
|
845
|
+
}
|
|
799
846
|
|
|
800
847
|
if (config.telegram.includeLastCcMessageInTelegram && event.last_assistant_message) {
|
|
801
848
|
const maxLen = 3500;
|
|
@@ -813,15 +860,24 @@ process.stdin.on('end', async () => {
|
|
|
813
860
|
telegramMessage += debugBlockHtml;
|
|
814
861
|
}
|
|
815
862
|
|
|
816
|
-
|
|
863
|
+
const webhookPayload = {
|
|
817
864
|
title: `${desktopStatus}: ${label}`,
|
|
818
865
|
project,
|
|
819
866
|
branch: branch || undefined,
|
|
820
867
|
duration,
|
|
821
868
|
trigger: eventType,
|
|
822
|
-
voicePhrase: config.voice.enabled ? getVoicePhrase(duration, project) : null,
|
|
823
869
|
hookEvent: event,
|
|
824
|
-
}
|
|
870
|
+
};
|
|
871
|
+
if (eventType === 'PermissionRequest') {
|
|
872
|
+
webhookPayload.toolName = permToolName;
|
|
873
|
+
webhookPayload.toolInput = event.tool_input || {};
|
|
874
|
+
}
|
|
875
|
+
if (config.voice.enabled) {
|
|
876
|
+
webhookPayload.voicePhrase = eventType === 'PermissionRequest'
|
|
877
|
+
? getPermissionVoicePhrase(project, permToolName)
|
|
878
|
+
: getVoicePhrase(duration, project);
|
|
879
|
+
}
|
|
880
|
+
await sendWebhook(config, webhookPayload);
|
|
825
881
|
|
|
826
882
|
state._telegramText = telegramMessage;
|
|
827
883
|
await sendTelegram(config, state);
|
|
@@ -833,5 +889,9 @@ process.stdin.on('end', async () => {
|
|
|
833
889
|
|
|
834
890
|
await sendDesktopNotification(config, desktopTitle, desktopMessage);
|
|
835
891
|
playSound(config);
|
|
836
|
-
|
|
892
|
+
if (eventType === 'PermissionRequest') {
|
|
893
|
+
speakText(config, getPermissionVoicePhrase(project, permToolName));
|
|
894
|
+
} else {
|
|
895
|
+
speakResult(config, duration, project);
|
|
896
|
+
}
|
|
837
897
|
});
|
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.74",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|