claude-notification-plugin 1.1.35 → 1.1.38

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.35",
3
+ "version": "1.1.38",
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",
@@ -9,5 +9,12 @@
9
9
  "homepage": "https://github.com/Bazilio-san/claude-notification-plugin#readme",
10
10
  "repository": "https://github.com/Bazilio-san/claude-notification-plugin",
11
11
  "license": "MIT",
12
- "keywords": ["notification", "telegram", "windows", "sound", "voice", "hooks"]
12
+ "keywords": [
13
+ "notification",
14
+ "telegram",
15
+ "windows",
16
+ "sound",
17
+ "voice",
18
+ "hooks"
19
+ ]
13
20
  }
package/README.md CHANGED
@@ -96,7 +96,9 @@ Config file: `~/.claude/claude-notify.config.json`
96
96
  "maxQueuePerWorkDir": 10,
97
97
  "maxTotalTasks": 50,
98
98
  "logDir": "abs-path-to-listener-logs",
99
- "taskLogDir": "abs-path-to-task-logs"
99
+ "taskLogDir": "abs-path-to-task-logs",
100
+ "liveConsole": true,
101
+ "liveConsoleInterval": 5
100
102
  }
101
103
  }
102
104
  ```
@@ -264,6 +266,8 @@ All commands start with `/` and execute instantly (not queued).
264
266
  | `maxTotalTasks` | `50` | Max tasks across all queues |
265
267
  | `logDir` | `~/.claude` | Listener log directory |
266
268
  | `taskLogDir` | same as `logDir` | Task Q&A log directory |
269
+ | `liveConsole` | `true` | Stream PTY output to the "Running..." Telegram message in real-time |
270
+ | `liveConsoleInterval`| `5` | Live console update interval in seconds |
267
271
 
268
272
  ### Projects and worktrees
269
273
 
package/commit-sha CHANGED
@@ -1 +1 @@
1
- 0c61ed57e3d709a6379f00009d55d8f6093f1cbc
1
+ 0fdb0002b24921b5b1c581fcd13720f2bd1247e0
@@ -6,7 +6,7 @@ import path from 'path';
6
6
  import process from 'process';
7
7
  import { createLogger } from './logger.js';
8
8
  import { createTaskLogger } from './task-logger.js';
9
- import { TelegramPoller, escapeHtml } from './telegram-poller.js';
9
+ import { TelegramPoller, escapeHtml, stripAnsi } from './telegram-poller.js';
10
10
  import { WorkQueue } from './work-queue.js';
11
11
  import { PtyRunner } from './pty-runner.js';
12
12
  import { WorktreeManager } from './worktree-manager.js';
@@ -104,16 +104,23 @@ const runner = new PtyRunner(logger, taskTimeout, taskLogger);
104
104
 
105
105
  const worktreeManager = new WorktreeManager(config, logger);
106
106
 
107
+ const liveConsoleEnabled = listenerConfig.liveConsole !== false; // default: true
108
+ const liveConsoleInterval = (listenerConfig.liveConsoleInterval || 5) * 1000;
109
+ const LIVE_CONSOLE_MAX_OUTPUT = 3000;
110
+
107
111
  const startTime = Date.now();
108
112
 
109
113
  // Session tracking per workDir: { taskCount, lastSessionId, lastContextPct }
110
114
  const sessions = new Map();
111
115
  // WorkDirs that should start a fresh session on next task
112
116
  const freshSessionDirs = new Set();
117
+ // Live console intervals per workDir
118
+ const liveConsoleTimers = new Map();
113
119
 
114
120
  logger.info('Listener started');
115
121
  logger.info(`Projects: ${JSON.stringify(Object.keys(listenerConfig.projects))}`);
116
122
  logger.info(`Session continuity: ${continueSessionEnabled ? 'enabled' : 'disabled'}`);
123
+ logger.info(`Live console: ${liveConsoleEnabled ? `enabled (${liveConsoleInterval / 1000}s interval)` : 'disabled'}`);
117
124
 
118
125
  // ----------------------
119
126
  // DISCOVER WORKTREES ON START
@@ -139,6 +146,7 @@ for (const { workDir, next } of recovered) {
139
146
  // ----------------------
140
147
 
141
148
  runner.on('complete', async (workDir, task, result) => {
149
+ stopLiveConsole(workDir);
142
150
  const entry = queue.queues[workDir];
143
151
  const label = formatLabel(entry);
144
152
 
@@ -209,6 +217,7 @@ runner.on('complete', async (workDir, task, result) => {
209
217
  });
210
218
 
211
219
  runner.on('error', async (workDir, task, errorMsg) => {
220
+ stopLiveConsole(workDir);
212
221
  const entry = queue.queues[workDir];
213
222
  const label = formatLabel(entry);
214
223
 
@@ -227,6 +236,7 @@ runner.on('error', async (workDir, task, errorMsg) => {
227
236
  });
228
237
 
229
238
  runner.on('timeout', async (workDir, task) => {
239
+ stopLiveConsole(workDir);
230
240
  const entry = queue.queues[workDir];
231
241
  const label = formatLabel(entry);
232
242
  const timeoutMin = Math.round(taskTimeout / 60000);
@@ -278,6 +288,55 @@ function shouldContinueSession (workDir) {
278
288
  return sessions.has(workDir);
279
289
  }
280
290
 
291
+ function startLiveConsole (workDir, messageId, header) {
292
+ stopLiveConsole(workDir);
293
+ if (!liveConsoleEnabled || !messageId) {
294
+ return;
295
+ }
296
+ let lastSentText = '';
297
+ const timer = setInterval(async () => {
298
+ try {
299
+ const raw = runner.getBuffer(workDir);
300
+ if (!raw) {
301
+ return;
302
+ }
303
+ const cleaned = stripAnsi(raw)
304
+ .split('\n')
305
+ .map((l) => l.trimEnd())
306
+ .filter((l) => l.length > 0)
307
+ .join('\n');
308
+ if (!cleaned) {
309
+ return;
310
+ }
311
+ // Take the tail that fits
312
+ const tail = cleaned.length > LIVE_CONSOLE_MAX_OUTPUT
313
+ ? cleaned.slice(-LIVE_CONSOLE_MAX_OUTPUT)
314
+ : cleaned;
315
+ // Trim to last complete line if we sliced mid-line
316
+ const output = cleaned.length > LIVE_CONSOLE_MAX_OUTPUT
317
+ ? tail.slice(tail.indexOf('\n') + 1)
318
+ : tail;
319
+ if (!output || output === lastSentText) {
320
+ return;
321
+ }
322
+ lastSentText = output;
323
+ const text = `${header}\n\n<pre>${escapeHtml(output)}</pre>`;
324
+ await poller.editMessage(messageId, text);
325
+ } catch {
326
+ // ignore edit errors — message may have been deleted
327
+ }
328
+ }, liveConsoleInterval);
329
+ liveConsoleTimers.set(workDir, timer);
330
+ }
331
+
332
+ function stopLiveConsole (workDir) {
333
+ const timer = liveConsoleTimers.get(workDir);
334
+ if (timer) {
335
+ clearInterval(timer);
336
+ liveConsoleTimers.delete(workDir);
337
+ }
338
+ }
339
+
281
340
  async function startTask (workDir, task) {
282
341
  const entry = queue.queues[workDir];
283
342
  const label = formatLabel(entry);
@@ -308,6 +367,7 @@ async function startTask (workDir, task) {
308
367
  }
309
368
 
310
369
  task.runningMessageId = runningMsgId;
370
+ startLiveConsole(workDir, runningMsgId, runningFull);
311
371
  const claudeArgs = getClaudeArgs(entry?.project);
312
372
  try {
313
373
  runner.run(workDir, task, claudeArgs, continueSession);
@@ -752,6 +812,9 @@ let running = true;
752
812
  process.on('SIGTERM', () => {
753
813
  logger.info('Received SIGTERM');
754
814
  running = false;
815
+ for (const wd of liveConsoleTimers.keys()) {
816
+ stopLiveConsole(wd);
817
+ }
755
818
  runner.cancelAll();
756
819
  setTimeout(() => process.exit(0), 2000);
757
820
  });
@@ -759,6 +822,9 @@ process.on('SIGTERM', () => {
759
822
  process.on('SIGINT', () => {
760
823
  logger.info('Received SIGINT');
761
824
  running = false;
825
+ for (const wd of liveConsoleTimers.keys()) {
826
+ stopLiveConsole(wd);
827
+ }
762
828
  runner.cancelAll();
763
829
  setTimeout(() => process.exit(0), 2000);
764
830
  });
@@ -209,8 +209,10 @@ export class PtyRunner extends EventEmitter {
209
209
  // Set up marker wait + timeout
210
210
  const markerPromise = this._waitForMarker(pendingId, this.timeout);
211
211
 
212
- // Send the task text to the PTY
213
- session.pty.write(task.text + '\r');
212
+ // Send the task text to the PTY using bracketed paste mode.
213
+ // Without this, newlines in the text are interpreted as Enter keypresses,
214
+ // splitting the prompt into multiple submissions and breaking the flow.
215
+ session.pty.write(`\x1b[200~${task.text}\x1b[201~\r`);
214
216
  this.logger.info(`PTY task sent to ${workDir}: ${task.text.slice(0, 100)}`);
215
217
 
216
218
  // Handle completion asynchronously
@@ -430,6 +432,14 @@ export class PtyRunner extends EventEmitter {
430
432
  return session?.currentTask || null;
431
433
  }
432
434
 
435
+ /**
436
+ * Get the raw PTY buffer for a workDir.
437
+ */
438
+ getBuffer (workDir) {
439
+ const session = this.sessions.get(workDir);
440
+ return session?._buffer || '';
441
+ }
442
+
433
443
  /**
434
444
  * Cancel all active tasks (for graceful shutdown).
435
445
  */
@@ -145,6 +145,43 @@ export class TelegramPoller {
145
145
  }
146
146
  }
147
147
 
148
+ async editMessage (messageId, text) {
149
+ if (!messageId) {
150
+ return false;
151
+ }
152
+ try {
153
+ const res = await fetch(`${this.baseUrl}/editMessageText`, {
154
+ method: 'POST',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: JSON.stringify({
157
+ chat_id: this.chatId,
158
+ message_id: messageId,
159
+ text,
160
+ parse_mode: 'HTML',
161
+ }),
162
+ });
163
+ const data = await res.json();
164
+ if (!data.ok) {
165
+ // Retry without HTML parse mode if formatting fails
166
+ const res2 = await fetch(`${this.baseUrl}/editMessageText`, {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify({
170
+ chat_id: this.chatId,
171
+ message_id: messageId,
172
+ text,
173
+ }),
174
+ });
175
+ const data2 = await res2.json();
176
+ return data2.ok;
177
+ }
178
+ return true;
179
+ } catch (err) {
180
+ this.logger.error(`editMessage error: ${err.message}`);
181
+ return false;
182
+ }
183
+ }
184
+
148
185
  async sendDocument (buffer, filename, caption) {
149
186
  try {
150
187
  const formData = new FormData();
@@ -200,4 +237,19 @@ function splitMessage (text) {
200
237
  return chunks;
201
238
  }
202
239
 
203
- export { escapeHtml };
240
+ // Strip ANSI escape codes and common terminal control sequences from PTY output
241
+ function stripAnsi (text) {
242
+ return text
243
+ // ANSI escape sequences (colors, cursor, etc.)
244
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
245
+ // OSC sequences (title setting, hyperlinks, etc.)
246
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
247
+ // Other escape sequences
248
+ .replace(/\x1b[^[\]]/g, '')
249
+ // Carriage returns (overwrite lines)
250
+ .replace(/\r/g, '')
251
+ // Remaining control chars except newline and tab
252
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
253
+ }
254
+
255
+ export { escapeHtml, stripAnsi };
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.35",
4
+ "version": "1.1.38",
5
5
  "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
6
  "type": "module",
7
7
  "engines": {