claude-notification-plugin 1.0.15 → 1.0.18
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/LICENSE +21 -21
- package/README.md +127 -121
- package/bin/install.js +178 -178
- package/bin/uninstall.js +53 -53
- package/notifier/notifier.js +340 -329
- package/package.json +1 -1
package/notifier/notifier.js
CHANGED
|
@@ -1,329 +1,340 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import process from 'process';
|
|
7
|
-
import notifier from 'node-notifier';
|
|
8
|
-
import player from 'play-sound';
|
|
9
|
-
import say from 'say';
|
|
10
|
-
|
|
11
|
-
const audio = player({});
|
|
12
|
-
|
|
13
|
-
// ----------------------
|
|
14
|
-
// CONFIG
|
|
15
|
-
// ----------------------
|
|
16
|
-
|
|
17
|
-
function loadConfig () {
|
|
18
|
-
const configPath = path.join(os.homedir(), '.claude', 'notifier.config.json');
|
|
19
|
-
|
|
20
|
-
const config = {
|
|
21
|
-
telegram: {
|
|
22
|
-
enabled: true,
|
|
23
|
-
token: '',
|
|
24
|
-
chatId: '',
|
|
25
|
-
deleteAfterHours: 24,
|
|
26
|
-
},
|
|
27
|
-
windowsNotification: {
|
|
28
|
-
enabled: true,
|
|
29
|
-
},
|
|
30
|
-
sound: {
|
|
31
|
-
enabled: true,
|
|
32
|
-
file: 'C:/Windows/Media/notify.wav',
|
|
33
|
-
},
|
|
34
|
-
voice: {
|
|
35
|
-
enabled: true,
|
|
36
|
-
},
|
|
37
|
-
minSeconds: 15,
|
|
38
|
-
notifyOnWaiting:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// ----------------------
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import process from 'process';
|
|
7
|
+
import notifier from 'node-notifier';
|
|
8
|
+
import player from 'play-sound';
|
|
9
|
+
import say from 'say';
|
|
10
|
+
|
|
11
|
+
const audio = player({});
|
|
12
|
+
|
|
13
|
+
// ----------------------
|
|
14
|
+
// CONFIG
|
|
15
|
+
// ----------------------
|
|
16
|
+
|
|
17
|
+
function loadConfig () {
|
|
18
|
+
const configPath = path.join(os.homedir(), '.claude', 'notifier.config.json');
|
|
19
|
+
|
|
20
|
+
const config = {
|
|
21
|
+
telegram: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
token: '',
|
|
24
|
+
chatId: '',
|
|
25
|
+
deleteAfterHours: 24,
|
|
26
|
+
},
|
|
27
|
+
windowsNotification: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
},
|
|
30
|
+
sound: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
file: 'C:/Windows/Media/notify.wav',
|
|
33
|
+
},
|
|
34
|
+
voice: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
},
|
|
37
|
+
minSeconds: 15,
|
|
38
|
+
notifyOnWaiting: false,
|
|
39
|
+
debug: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (fs.existsSync(configPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
45
|
+
const user = JSON.parse(raw);
|
|
46
|
+
if (user.telegram) {
|
|
47
|
+
config.telegram = { ...config.telegram, ...user.telegram };
|
|
48
|
+
}
|
|
49
|
+
if (user.windowsNotification) {
|
|
50
|
+
config.windowsNotification = { ...config.windowsNotification, ...user.windowsNotification };
|
|
51
|
+
}
|
|
52
|
+
if (user.sound) {
|
|
53
|
+
config.sound = { ...config.sound, ...user.sound };
|
|
54
|
+
}
|
|
55
|
+
if (user.voice) {
|
|
56
|
+
config.voice = { ...config.voice, ...user.voice };
|
|
57
|
+
}
|
|
58
|
+
if (typeof user.minSeconds === 'number') {
|
|
59
|
+
config.minSeconds = user.minSeconds;
|
|
60
|
+
}
|
|
61
|
+
if (typeof user.notifyOnWaiting === 'boolean') {
|
|
62
|
+
config.notifyOnWaiting = user.notifyOnWaiting;
|
|
63
|
+
}
|
|
64
|
+
if (typeof user.debug === 'boolean') {
|
|
65
|
+
config.debug = user.debug;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore malformed config
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (process.env.TELEGRAM_TOKEN) {
|
|
73
|
+
config.telegram.token = process.env.TELEGRAM_TOKEN;
|
|
74
|
+
}
|
|
75
|
+
if (process.env.TELEGRAM_CHAT_ID) {
|
|
76
|
+
config.telegram.chatId = process.env.TELEGRAM_CHAT_ID;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Per-channel env overrides (0 = off, 1 = on)
|
|
80
|
+
if (process.env.CLAUDE_NOTIFY_TELEGRAM !== undefined) {
|
|
81
|
+
config.telegram.enabled = process.env.CLAUDE_NOTIFY_TELEGRAM === '1';
|
|
82
|
+
}
|
|
83
|
+
if (process.env.CLAUDE_NOTIFY_WINDOWS !== undefined) {
|
|
84
|
+
config.windowsNotification.enabled = process.env.CLAUDE_NOTIFY_WINDOWS === '1';
|
|
85
|
+
}
|
|
86
|
+
if (process.env.CLAUDE_NOTIFY_SOUND !== undefined) {
|
|
87
|
+
config.sound.enabled = process.env.CLAUDE_NOTIFY_SOUND === '1';
|
|
88
|
+
}
|
|
89
|
+
if (process.env.CLAUDE_NOTIFY_VOICE !== undefined) {
|
|
90
|
+
config.voice.enabled = process.env.CLAUDE_NOTIFY_VOICE === '1';
|
|
91
|
+
}
|
|
92
|
+
if (process.env.CLAUDE_NOTIFY_WAITING !== undefined) {
|
|
93
|
+
config.notifyOnWaiting = process.env.CLAUDE_NOTIFY_WAITING === '1';
|
|
94
|
+
}
|
|
95
|
+
if (process.env.CLAUDE_NOTIFY_DEBUG !== undefined) {
|
|
96
|
+
config.debug = process.env.CLAUDE_NOTIFY_DEBUG === '1';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return config;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ----------------------
|
|
103
|
+
// PROJECT-LEVEL DISABLE
|
|
104
|
+
// ----------------------
|
|
105
|
+
|
|
106
|
+
function isNotifierDisabled () {
|
|
107
|
+
return process.env.DISABLE_CLAUDE_NOTIFIER === '1'
|
|
108
|
+
|| process.env.DISABLE_CLAUDE_NOTIFIER === 'true';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ----------------------
|
|
112
|
+
// STATE FILE
|
|
113
|
+
// ----------------------
|
|
114
|
+
|
|
115
|
+
const STATE_FILE = path.join(
|
|
116
|
+
os.homedir(),
|
|
117
|
+
'.claude',
|
|
118
|
+
'.notifier_state.json',
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
function loadState () {
|
|
122
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
|
|
125
|
+
} catch {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function saveState (state) {
|
|
133
|
+
const dir = path.dirname(STATE_FILE);
|
|
134
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
135
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ----------------------
|
|
139
|
+
// TELEGRAM
|
|
140
|
+
// ----------------------
|
|
141
|
+
|
|
142
|
+
async function sendTelegram (config, state) {
|
|
143
|
+
if (!config.telegram.enabled || !config.telegram.token || !config.telegram.chatId) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const baseUrl = `https://api.telegram.org/bot${config.telegram.token}`;
|
|
148
|
+
|
|
149
|
+
// Send new message and store its id
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch(`${baseUrl}/sendMessage`, {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
chat_id: config.telegram.chatId,
|
|
156
|
+
text: state._telegramText,
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
const data = await res.json();
|
|
160
|
+
if (data.ok && data.result?.message_id) {
|
|
161
|
+
if (!state.sentMessages) {
|
|
162
|
+
state.sentMessages = [];
|
|
163
|
+
}
|
|
164
|
+
state.sentMessages.push({
|
|
165
|
+
id: data.result.message_id,
|
|
166
|
+
ts: Date.now(),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// silent fail
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Delete old messages
|
|
174
|
+
const maxAge = (config.telegram.deleteAfterHours || 24) * 3600_000;
|
|
175
|
+
if (state.sentMessages?.length) {
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const keep = [];
|
|
178
|
+
for (const msg of state.sentMessages) {
|
|
179
|
+
if (now - msg.ts > maxAge) {
|
|
180
|
+
try {
|
|
181
|
+
await fetch(`${baseUrl}/deleteMessage`, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
chat_id: config.telegram.chatId,
|
|
186
|
+
message_id: msg.id,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
} catch {
|
|
190
|
+
// silent fail
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
keep.push(msg);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
state.sentMessages = keep;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ----------------------
|
|
201
|
+
// WINDOWS NOTIFICATION
|
|
202
|
+
// ----------------------
|
|
203
|
+
|
|
204
|
+
function sendWindowsNotification (config, message) {
|
|
205
|
+
if (!config.windowsNotification.enabled) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
notifier.notify({
|
|
209
|
+
title: 'Claude Code',
|
|
210
|
+
message,
|
|
211
|
+
sound: true,
|
|
212
|
+
wait: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ----------------------
|
|
217
|
+
// SOUND & VOICE
|
|
218
|
+
// ----------------------
|
|
219
|
+
|
|
220
|
+
function playSound (config) {
|
|
221
|
+
if (!config.sound.enabled) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
audio.play(config.sound.file);
|
|
226
|
+
} catch {
|
|
227
|
+
// silent fail
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const voicePhrases = {
|
|
232
|
+
en: (d) => `Claude finished coding in ${d} seconds`,
|
|
233
|
+
ru: (d) => `Клод завершил работу за ${d} секунд`,
|
|
234
|
+
de: (d) => `Claude hat die Arbeit in ${d} Sekunden abgeschlossen`,
|
|
235
|
+
fr: (d) => `Claude a termine en ${d} secondes`,
|
|
236
|
+
es: (d) => `Claude termino en ${d} segundos`,
|
|
237
|
+
pt: (d) => `Claude terminou em ${d} segundos`,
|
|
238
|
+
ja: (d) => `Claudeは${d}秒でコーディングを完了しました`,
|
|
239
|
+
ko: (d) => `Claude가 ${d}초 만에 코딩을 완료했습니다`,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
function getVoicePhrase (duration) {
|
|
243
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en';
|
|
244
|
+
const lang = locale.split('-')[0].toLowerCase();
|
|
245
|
+
const fn = voicePhrases[lang] || voicePhrases.en;
|
|
246
|
+
return fn(duration);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function speakResult (config, duration) {
|
|
250
|
+
if (!config.voice.enabled) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
say.speak(getVoicePhrase(duration));
|
|
255
|
+
} catch {
|
|
256
|
+
// silent fail
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ----------------------
|
|
261
|
+
// READ HOOK INPUT
|
|
262
|
+
// ----------------------
|
|
263
|
+
|
|
264
|
+
let input = '';
|
|
265
|
+
|
|
266
|
+
process.stdin.on('data', (chunk) => {
|
|
267
|
+
input += chunk;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
process.stdin.on('end', async () => {
|
|
271
|
+
const config = loadConfig();
|
|
272
|
+
|
|
273
|
+
let event = {};
|
|
274
|
+
try {
|
|
275
|
+
event = JSON.parse(input);
|
|
276
|
+
} catch {
|
|
277
|
+
// ignore
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const eventType = event.hook_event_name || 'unknown';
|
|
281
|
+
const cwd = event.cwd || process.cwd();
|
|
282
|
+
const project = path.basename(cwd);
|
|
283
|
+
|
|
284
|
+
if (isNotifierDisabled()) {
|
|
285
|
+
process.exit(0);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const state = loadState();
|
|
289
|
+
|
|
290
|
+
// ----------------------
|
|
291
|
+
// START TIMER
|
|
292
|
+
// ----------------------
|
|
293
|
+
|
|
294
|
+
if (eventType === 'UserPromptSubmit') {
|
|
295
|
+
state.start = Date.now();
|
|
296
|
+
saveState(state);
|
|
297
|
+
process.exit(0);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ----------------------
|
|
301
|
+
// STOP / NOTIFICATION EVENT
|
|
302
|
+
// ----------------------
|
|
303
|
+
|
|
304
|
+
if (eventType !== 'Stop' && eventType !== 'Notification') {
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (eventType === 'Notification' && !config.notifyOnWaiting) {
|
|
309
|
+
process.exit(0);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let duration = 0;
|
|
313
|
+
if (state.start) {
|
|
314
|
+
duration = Math.round((Date.now() - state.start) / 1000);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (duration < config.minSeconds) {
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const title = eventType === 'Notification'
|
|
322
|
+
? 'Claude waiting for input'
|
|
323
|
+
: 'Claude finished coding';
|
|
324
|
+
|
|
325
|
+
let message =
|
|
326
|
+
`${title}\n\nProject: ${project}\nDuration: ${duration}s\nTrigger: ${eventType}`;
|
|
327
|
+
|
|
328
|
+
if (config.debug && config.voice.enabled) {
|
|
329
|
+
message += `\nVoice: ${getVoicePhrase(duration)}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
state._telegramText = `\u{1F916} ${message}`;
|
|
333
|
+
await sendTelegram(config, state);
|
|
334
|
+
delete state._telegramText;
|
|
335
|
+
saveState(state);
|
|
336
|
+
|
|
337
|
+
sendWindowsNotification(config, message);
|
|
338
|
+
playSound(config);
|
|
339
|
+
speakResult(config, duration);
|
|
340
|
+
});
|