claude-notification-plugin 1.1.55 → 1.1.57

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.55",
3
+ "version": "1.1.57",
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
@@ -98,7 +98,8 @@ Config file: `~/.claude/claude-notify.config.json`
98
98
  "logDir": "abs-path-to-listener-logs",
99
99
  "taskLogDir": "abs-path-to-task-logs",
100
100
  "liveConsole": true,
101
- "liveConsoleInterval": 5
101
+ "liveConsoleIntervalMillis": 1,
102
+ "liveConsoleMaxOutputChars": 300
102
103
  }
103
104
  }
104
105
  ```
@@ -271,7 +272,8 @@ Projects are referenced with the `&` prefix (e.g. `&api`, `&api/branch`).
271
272
  | `logDir` | `~/.claude` | Listener log directory |
272
273
  | `taskLogDir` | same as `logDir` | Task Q&A log directory |
273
274
  | `liveConsole` | `true` | Stream PTY output + tool activity to the "Running..." Telegram message in real-time |
274
- | `liveConsoleInterval`| `5` | Live console update interval in seconds |
275
+ | `liveConsoleIntervalMillis`| `1` | Live console update interval in seconds |
276
+ | `liveConsoleMaxOutputChars`| `300` | Max characters of PTY output to show in live console |
275
277
 
276
278
 
277
279
  ### Projects and worktrees
package/commit-sha CHANGED
@@ -1 +1 @@
1
- f1a404d4c36cb1cdba3222fcb03a8da92a560b7b
1
+ f3f8f93367ed5e932cc772b3bb8568dcc7f0da0f
@@ -768,7 +768,8 @@ The output is cleaned from ANSI escape codes and Claude Code UI chrome (logo, st
768
768
 
769
769
  Configuration:
770
770
  - `liveConsole` — enable/disable (default: `true`)
771
- - `liveConsoleInterval` — update interval in seconds (default: `5`)
771
+ - `liveConsoleIntervalMillis` — update interval in seconds (default: `1`)
772
+ - `liveConsoleMaxOutputChars` — max characters of PTY output to show (default: `300`)
772
773
 
773
774
  ### PTY logs
774
775
 
@@ -127,8 +127,8 @@ const runner = new PtyRunner(logger, taskTimeout, taskLogger, taskLogDir);
127
127
  const worktreeManager = new WorktreeManager(config, logger);
128
128
 
129
129
  const liveConsoleEnabled = listenerConfig.liveConsole !== false; // default: true
130
- const liveConsoleInterval = (listenerConfig.liveConsoleInterval || 5) * 1000;
131
- const LIVE_CONSOLE_MAX_OUTPUT = 3000;
130
+ const liveConsoleIntervalMillis = (listenerConfig.liveConsoleIntervalMillis || 1) * 1000;
131
+ const liveConsoleMaxOutputChars = listenerConfig.liveConsoleMaxOutputChars || 300;
132
132
 
133
133
  const startTime = Date.now();
134
134
 
@@ -142,7 +142,7 @@ const liveConsoleTimers = new Map();
142
142
  logger.info('Listener started');
143
143
  logger.info(`Projects: ${JSON.stringify(Object.keys(listenerConfig.projects))}`);
144
144
  logger.info(`Session continuity: ${continueSessionEnabled ? 'enabled' : 'disabled'}`);
145
- logger.info(`Live console: ${liveConsoleEnabled ? `enabled (${liveConsoleInterval / 1000}s interval)` : 'disabled'}`);
145
+ logger.info(`Live console: ${liveConsoleEnabled ? `enabled (${liveConsoleIntervalMillis / 1000}s interval, max ${liveConsoleMaxOutputChars} chars)` : 'disabled'}`);
146
146
 
147
147
  // ----------------------
148
148
  // DISCOVER WORKTREES ON START
@@ -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) {
@@ -339,11 +339,11 @@ function startLiveConsole (workDir, messageId, header) {
339
339
  return;
340
340
  }
341
341
  // Take the tail that fits
342
- const tail = cleaned.length > LIVE_CONSOLE_MAX_OUTPUT
343
- ? cleaned.slice(-LIVE_CONSOLE_MAX_OUTPUT)
342
+ const tail = cleaned.length > liveConsoleMaxOutputChars
343
+ ? cleaned.slice(-liveConsoleMaxOutputChars)
344
344
  : cleaned;
345
345
  // Trim to last complete line if we sliced mid-line
346
- const output = cleaned.length > LIVE_CONSOLE_MAX_OUTPUT
346
+ const output = cleaned.length > liveConsoleMaxOutputChars
347
347
  ? tail.slice(tail.indexOf('\n') + 1)
348
348
  : tail;
349
349
  if (!output || output === lastSentText) {
@@ -360,7 +360,7 @@ function startLiveConsole (workDir, messageId, header) {
360
360
  } catch (err) {
361
361
  logger.warn(`Live console edit error: ${err.message}`);
362
362
  }
363
- }, liveConsoleInterval);
363
+ }, liveConsoleIntervalMillis);
364
364
  liveConsoleTimers.set(workDir, timer);
365
365
  }
366
366
 
@@ -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);
@@ -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
- return text
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
- // Carriage returns (overwrite lines)
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
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.55",
4
+ "version": "1.1.57",
5
5
  "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
6
  "type": "module",
7
7
  "engines": {