claude-notification-plugin 1.1.56 → 1.1.58

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.1.56",
3
+ "version": "1.1.58",
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/commit-sha CHANGED
@@ -1 +1 @@
1
- dcf4136a64282b07c755d01b99b1aba6fa0e7de1
1
+ 62dff0d5bb6a43cead78d21f435af9110212e139
@@ -277,7 +277,7 @@ runner.on('timeout', async (workDir, task) => {
277
277
 
278
278
  await poller.deleteMessage(task.runningMessageId);
279
279
 
280
- const headerShort = `⏰ <code>${label}</code>\nTask forcefully stopped — timeout exceeded (${timeoutMin} min)`;
280
+ const headerShort = `⏰ <code>${label}</code>\nTask forcefully stopped — no activity for ${timeoutMin} min`;
281
281
  const headerFull = `${headerShort}: ${escapeHtml(task.text)}`;
282
282
  const sentId = await poller.sendMessage(headerShort, task.telegramMessageId);
283
283
  if (!sentId && task.telegramMessageId) {
@@ -106,6 +106,7 @@ export class PtyRunner extends EventEmitter {
106
106
  for (const [pid, resolve] of this.pendingMarkers) {
107
107
  const session = this._findSessionByPendingId(pid);
108
108
  if (session && this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
109
+ session._lastActivityTime = Date.now();
109
110
  this.pendingMarkers.delete(pid);
110
111
  this._unlinkSafe(filePath);
111
112
  resolve(marker);
@@ -138,6 +139,7 @@ export class PtyRunner extends EventEmitter {
138
139
  this._unlinkSafe(filePath);
139
140
  for (const [workDir, session] of this.sessions) {
140
141
  if (this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
142
+ session._lastActivityTime = Date.now();
141
143
  session._model = marker.model || '';
142
144
  this.emit('ready', workDir, marker);
143
145
  break;
@@ -147,6 +149,7 @@ export class PtyRunner extends EventEmitter {
147
149
  // PostToolUse — update activity data (don't delete, gets overwritten)
148
150
  for (const [, session] of this.sessions) {
149
151
  if (this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
152
+ session._lastActivityTime = Date.now();
150
153
  session._lastActivity = {
151
154
  toolName: marker.toolName,
152
155
  toolInput: marker.toolInput,
@@ -160,6 +163,7 @@ export class PtyRunner extends EventEmitter {
160
163
  this._unlinkSafe(filePath);
161
164
  for (const [workDir, session] of this.sessions) {
162
165
  if (this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
166
+ session._lastActivityTime = Date.now();
163
167
  session._lastCompact = {
164
168
  summary: marker.summary,
165
169
  trigger: marker.trigger,
@@ -197,15 +201,28 @@ export class PtyRunner extends EventEmitter {
197
201
  /**
198
202
  * Wait for a marker file for the given pending ID.
199
203
  */
200
- _waitForMarker (pendingId, timeoutMs) {
204
+ /**
205
+ * Wait for a marker file with inactivity-based timeout.
206
+ * Timer resets on any PTY output or hook signal activity.
207
+ */
208
+ _waitForMarker (pendingId, inactivityMs, session) {
201
209
  return new Promise((resolve, reject) => {
202
- const timer = setTimeout(() => {
203
- this.pendingMarkers.delete(pendingId);
204
- reject(new Error('Marker timeout'));
205
- }, timeoutMs);
210
+ if (session) {
211
+ session._lastActivityTime = Date.now();
212
+ }
213
+
214
+ const CHECK_INTERVAL = 5000;
215
+ const checker = setInterval(() => {
216
+ const lastActivity = session?._lastActivityTime || 0;
217
+ if (lastActivity > 0 && Date.now() - lastActivity > inactivityMs) {
218
+ clearInterval(checker);
219
+ this.pendingMarkers.delete(pendingId);
220
+ reject(new Error('Marker timeout'));
221
+ }
222
+ }, CHECK_INTERVAL);
206
223
 
207
224
  this.pendingMarkers.set(pendingId, (marker) => {
208
- clearTimeout(timer);
225
+ clearInterval(checker);
209
226
  resolve(marker);
210
227
  });
211
228
  });
@@ -297,8 +314,8 @@ export class PtyRunner extends EventEmitter {
297
314
  session._buffer = '';
298
315
  this._openPtyLog(session, task);
299
316
 
300
- // Set up marker wait + timeout
301
- const markerPromise = this._waitForMarker(pendingId, this.timeout);
317
+ // Set up marker wait + inactivity timeout
318
+ const markerPromise = this._waitForMarker(pendingId, this.timeout, session);
302
319
 
303
320
  // Send the task text to the PTY.
304
321
  // Bracketed paste mode (\x1b[200~...\x1b[201~) causes Claude to hang in ConPTY,
@@ -435,6 +452,7 @@ export class PtyRunner extends EventEmitter {
435
452
 
436
453
  ptyProcess.onData((data) => {
437
454
  session._buffer += data;
455
+ session._lastActivityTime = Date.now();
438
456
  // Keep buffer reasonable size
439
457
  if (session._buffer.length > 50000) {
440
458
  session._buffer = session._buffer.slice(-25000);
@@ -624,28 +624,28 @@ function numberToWords (n, lang) {
624
624
  }
625
625
 
626
626
  const voicePhrases = {
627
- en: (d) => `Claude finished coding in ${numberToWords(d, 'en')} ${pluralize(d, ['second', 'seconds'])}`,
628
- ru: (d) => `Клод завершил работу за ${numberToWords(d, 'ru')} ${pluralize(d, ['секунду', 'секунды', 'секунд'])}`,
629
- de: (d) => `Claude hat die Arbeit in ${d} ${pluralize(d, ['Sekunde', 'Sekunden'])} abgeschlossen`,
630
- fr: (d) => `Claude a termine en ${d} ${pluralize(d, ['seconde', 'secondes'])}`,
631
- es: (d) => `Claude termino en ${d} ${pluralize(d, ['segundo', 'segundos'])}`,
632
- pt: (d) => `Claude terminou em ${d} ${pluralize(d, ['segundo', 'segundos'])}`,
633
- ja: (d) => `Claudeは${d}秒でコーディングを完了しました`,
634
- ko: (d) => `Claude가 ${d}초 만에 코딩을 완료했습니다`,
627
+ en: (d, p) => `Claude finished working on ${p} in ${numberToWords(d, 'en')} ${pluralize(d, ['second', 'seconds'])}`,
628
+ ru: (d, p) => `Клод завершил работу над проектом ${p} за ${numberToWords(d, 'ru')} ${pluralize(d, ['секунду', 'секунды', 'секунд'])}`,
629
+ de: (d, p) => `Claude hat die Arbeit an ${p} in ${d} ${pluralize(d, ['Sekunde', 'Sekunden'])} abgeschlossen`,
630
+ fr: (d, p) => `Claude a termine ${p} en ${d} ${pluralize(d, ['seconde', 'secondes'])}`,
631
+ es: (d, p) => `Claude termino ${p} en ${d} ${pluralize(d, ['segundo', 'segundos'])}`,
632
+ pt: (d, p) => `Claude terminou ${p} em ${d} ${pluralize(d, ['segundo', 'segundos'])}`,
633
+ ja: (d, p) => `Claudeは${p}の作業を${d}秒で完了しました`,
634
+ ko: (d, p) => `Claude가 ${p} 작업을 ${d}초 만에 완료했습니다`,
635
635
  };
636
636
 
637
- function getVoicePhrase (duration) {
637
+ function getVoicePhrase (duration, project) {
638
638
  const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en';
639
639
  const lang = locale.split('-')[0].toLowerCase();
640
640
  const fn = voicePhrases[lang] || voicePhrases.en;
641
- return fn(duration);
641
+ return fn(duration, project || 'unknown');
642
642
  }
643
643
 
644
- function speakResult (config, duration) {
644
+ function speakResult (config, duration, project) {
645
645
  if (!config.voice.enabled) {
646
646
  return;
647
647
  }
648
- const text = getVoicePhrase(duration);
648
+ const text = getVoicePhrase(duration, project);
649
649
  try {
650
650
  switch (PLATFORM) {
651
651
  case 'win32': {
@@ -808,7 +808,7 @@ process.stdin.on('end', async () => {
808
808
 
809
809
  if (config.debug) {
810
810
  const debugBlockHtml = '\n\n<b>Debug:</b>\n'
811
- + (config.voice.enabled ? `\nVoice: ${escapeHtml(getVoicePhrase(duration))}` : '')
811
+ + (config.voice.enabled ? `\nVoice: ${escapeHtml(getVoicePhrase(duration, project))}` : '')
812
812
  + `\n\nHook input:\n<pre>${escapeHtml(JSON.stringify(event, null, 2))}</pre>`;
813
813
  telegramMessage += debugBlockHtml;
814
814
  }
@@ -819,7 +819,7 @@ process.stdin.on('end', async () => {
819
819
  branch: branch || undefined,
820
820
  duration,
821
821
  trigger: eventType,
822
- voicePhrase: config.voice.enabled ? getVoicePhrase(duration) : null,
822
+ voicePhrase: config.voice.enabled ? getVoicePhrase(duration, project) : null,
823
823
  hookEvent: event,
824
824
  });
825
825
 
@@ -833,5 +833,5 @@ process.stdin.on('end', async () => {
833
833
 
834
834
  await sendDesktopNotification(config, desktopTitle, desktopMessage);
835
835
  playSound(config);
836
- speakResult(config, duration);
836
+ speakResult(config, duration, project);
837
837
  });
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.56",
4
+ "version": "1.1.58",
5
5
  "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
6
  "type": "module",
7
7
  "engines": {