claude-notification-plugin 1.1.35 → 1.1.39

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.39",
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
  ```
@@ -246,6 +248,7 @@ All commands start with `/` and execute instantly (not queued).
246
248
  | `/worktrees /project` | List worktrees |
247
249
  | `/worktree /project/branch` | Create a worktree |
248
250
  | `/rmworktree /project/branch` | Remove a worktree |
251
+ | `/pty [/project[/branch]]` | PTY session diagnostics (state, buffer, output) |
249
252
  | `/history` | Recent task history |
250
253
  | `/stop` | Stop the listener |
251
254
  | `/help` | Show help |
@@ -264,6 +267,9 @@ All commands start with `/` and execute instantly (not queued).
264
267
  | `maxTotalTasks` | `50` | Max tasks across all queues |
265
268
  | `logDir` | `~/.claude` | Listener log directory |
266
269
  | `taskLogDir` | same as `logDir` | Task Q&A log directory |
270
+ | `liveConsole` | `true` | Stream PTY output to the "Running..." Telegram message in real-time |
271
+ | `liveConsoleInterval`| `5` | Live console update interval in seconds |
272
+
267
273
 
268
274
  ### Projects and worktrees
269
275
 
package/commit-sha CHANGED
@@ -1 +1 @@
1
- 0c61ed57e3d709a6379f00009d55d8f6093f1cbc
1
+ 99358845aa9de2dfad99bbc5091580dcc91df4ed
@@ -20,6 +20,7 @@ and executes them on your machine via an interactive Claude Code PTY session. Th
20
20
  - [Projects and worktrees](#projects-and-worktrees)
21
21
  - [Task queues](#task-queues)
22
22
  - [Bot commands](#bot-commands)
23
+ - [Live console and PTY diagnostics](#live-console-and-pty-diagnostics)
23
24
  - [Task lifecycle](#task-lifecycle)
24
25
  - [State files](#state-files)
25
26
  - [Security](#security)
@@ -722,12 +723,75 @@ Bot: 👋 Listener is shutting down...
722
723
 
723
724
  All active tasks will be terminated. Queues are saved to disk and will be restored on the next startup.
724
725
 
726
+ ### /pty — PTY diagnostics
727
+
728
+ Shows real-time information about PTY sessions: state, buffer size, live console status, and the last 15 lines of cleaned output.
729
+
730
+ ```
731
+ You: /pty
732
+ Bot: 🖥 PTY Sessions:
733
+
734
+ /api
735
+ State: busy
736
+ Buffer: 12480 bytes
737
+ Elapsed: 2m 35s
738
+ Live console: ✅
739
+ PTY log: writing
740
+
741
+ ◐ Reading src/auth.js
742
+ ● Editing src/middleware.js
743
+ Added JWT validation...
744
+ ```
745
+
746
+ ```
747
+ You: /pty /api
748
+ Bot: (same, but for a specific project)
749
+ ```
750
+
725
751
  ### /help — help
726
752
 
727
753
  Shows a brief reference for all commands.
728
754
 
729
755
  ---
730
756
 
757
+ ## Live console and PTY diagnostics
758
+
759
+ ### Live console
760
+
761
+ When **`liveConsole`** is enabled (default: `true`), the "⏳ Running..." message in Telegram is periodically updated with the cleaned tail of Claude Code's PTY output, so you can see what Claude is doing in real-time.
762
+
763
+ The output is cleaned from ANSI escape codes and Claude Code UI chrome (logo, status bar, prompts), leaving only meaningful content.
764
+
765
+ Configuration:
766
+ - `liveConsole` — enable/disable (default: `true`)
767
+ - `liveConsoleInterval` — update interval in seconds (default: `5`)
768
+
769
+ ### PTY logs
770
+
771
+ Each running task writes raw PTY output to a file: `{taskLogDir}/{project}_{branch}_pty.log`.
772
+ The file is overwritten when a new task starts for the same project/branch.
773
+
774
+ Monitor in real-time:
775
+ ```bash
776
+ # Linux / macOS / Git Bash
777
+ tail -f ~/.claude/myproject_main_pty.log
778
+
779
+ # Windows PowerShell
780
+ Get-Content ~/.claude/myproject_main_pty.log -Wait -Tail 50
781
+ ```
782
+
783
+ ### /pty command
784
+
785
+ Send `/pty` or `/pty /project` in Telegram to get instant diagnostics:
786
+ - Session state (`busy` / `idle` / `starting`)
787
+ - Buffer size in bytes
788
+ - Elapsed time since task start
789
+ - Whether live console interval is active
790
+ - Whether PTY log stream is writing
791
+ - Last 15 lines of cleaned output
792
+
793
+ ---
794
+
731
795
  ## Task lifecycle
732
796
 
733
797
  ### Path of a task from message to result
@@ -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, cleanPtyOutput } 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';
@@ -100,20 +100,27 @@ const taskLogDir = config.listener?.taskLogDir || listenerLogDir;
100
100
  fs.mkdirSync(taskLogDir, { recursive: true });
101
101
  const taskLogger = createTaskLogger(taskLogDir);
102
102
 
103
- const runner = new PtyRunner(logger, taskTimeout, taskLogger);
103
+ const runner = new PtyRunner(logger, taskTimeout, taskLogger, taskLogDir);
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,52 @@ 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 = cleanPtyOutput(raw);
304
+ if (!cleaned) {
305
+ return;
306
+ }
307
+ // Take the tail that fits
308
+ const tail = cleaned.length > LIVE_CONSOLE_MAX_OUTPUT
309
+ ? cleaned.slice(-LIVE_CONSOLE_MAX_OUTPUT)
310
+ : cleaned;
311
+ // Trim to last complete line if we sliced mid-line
312
+ const output = cleaned.length > LIVE_CONSOLE_MAX_OUTPUT
313
+ ? tail.slice(tail.indexOf('\n') + 1)
314
+ : tail;
315
+ if (!output || output === lastSentText) {
316
+ return;
317
+ }
318
+ lastSentText = output;
319
+ const elapsed = formatDuration(Date.now() - new Date(runner.getActive(workDir)?.startedAt || Date.now()).getTime());
320
+ const text = `${header}\n<i>${elapsed}</i>\n\n<pre>${escapeHtml(output)}</pre>`;
321
+ await poller.editMessage(messageId, text);
322
+ } catch (err) {
323
+ logger.warn(`Live console edit error: ${err.message}`);
324
+ }
325
+ }, liveConsoleInterval);
326
+ liveConsoleTimers.set(workDir, timer);
327
+ }
328
+
329
+ function stopLiveConsole (workDir) {
330
+ const timer = liveConsoleTimers.get(workDir);
331
+ if (timer) {
332
+ clearInterval(timer);
333
+ liveConsoleTimers.delete(workDir);
334
+ }
335
+ }
336
+
281
337
  async function startTask (workDir, task) {
282
338
  const entry = queue.queues[workDir];
283
339
  const label = formatLabel(entry);
@@ -308,6 +364,7 @@ async function startTask (workDir, task) {
308
364
  }
309
365
 
310
366
  task.runningMessageId = runningMsgId;
367
+ startLiveConsole(workDir, runningMsgId, runningFull);
311
368
  const claudeArgs = getClaudeArgs(entry?.project);
312
369
  try {
313
370
  runner.run(workDir, task, claudeArgs, continueSession);
@@ -355,6 +412,8 @@ async function handleCommand (cmd, args) {
355
412
  return handleRemoveWorktree(args);
356
413
  case '/history':
357
414
  return handleHistory();
415
+ case '/pty':
416
+ return handlePty(args);
358
417
  case '/stop':
359
418
  return handleStop();
360
419
  case '/help':
@@ -631,6 +690,63 @@ function handleRemoveWorktree (args) {
631
690
  }
632
691
  }
633
692
 
693
+ function handlePty (args) {
694
+ const target = parseTarget(args);
695
+
696
+ if (target) {
697
+ let workDir;
698
+ try {
699
+ workDir = worktreeManager.resolveWorkDir(target.project, target.branch);
700
+ } catch (err) {
701
+ return `❌ ${escapeHtml(err.message)}`;
702
+ }
703
+ const info = runner.getSessionInfo(workDir);
704
+ if (!info) {
705
+ return `🖥 No PTY session for /${escapeHtml(target.project)}${target.branch ? '/' + escapeHtml(target.branch) : ''}`;
706
+ }
707
+ return formatPtyInfo(target.project, target.branch, workDir, info);
708
+ }
709
+
710
+ // All sessions
711
+ const allInfo = runner.getAllSessionInfo();
712
+ if (Object.keys(allInfo).length === 0) {
713
+ return '🖥 No active PTY sessions';
714
+ }
715
+
716
+ let text = '🖥 <b>PTY Sessions:</b>\n';
717
+ for (const [workDir, info] of Object.entries(allInfo)) {
718
+ const entry = queue.queues[workDir];
719
+ const project = entry?.project || '?';
720
+ const branch = entry?.branch || null;
721
+ text += '\n' + formatPtyInfo(project, branch, workDir, info);
722
+ }
723
+ return text;
724
+ }
725
+
726
+ function formatPtyInfo (project, branch, workDir, info) {
727
+ const label = branch && branch !== 'main' && branch !== 'master'
728
+ ? `/${project}/${branch}`
729
+ : `/${project}`;
730
+ const elapsed = info.startedAt
731
+ ? formatDuration(Date.now() - new Date(info.startedAt).getTime())
732
+ : '-';
733
+ const liveTimer = liveConsoleTimers.has(workDir) ? '✅' : '❌';
734
+ const raw = runner.getBuffer(workDir);
735
+ const cleaned = raw ? cleanPtyOutput(raw) : '';
736
+ const lastLines = cleaned
737
+ ? cleaned.split('\n').slice(-15).join('\n')
738
+ : '(empty)';
739
+
740
+ return `<b>${escapeHtml(label)}</b>
741
+ State: <code>${info.state}</code>
742
+ Buffer: <code>${info.bufferSize}</code> bytes
743
+ Elapsed: ${elapsed}
744
+ Live console: ${liveTimer}
745
+ PTY log: <code>${info.hasLogStream ? 'writing' : 'off'}</code>
746
+
747
+ <pre>${escapeHtml(lastLines)}</pre>`;
748
+ }
749
+
634
750
  function handleHistory () {
635
751
  const history = queue.getHistory(10);
636
752
  if (history.length === 0) {
@@ -669,6 +785,7 @@ function handleHelp () {
669
785
  /worktrees /project — project worktrees
670
786
  /worktree /project/branch — create worktree
671
787
  /rmworktree /project/branch — remove worktree
788
+ /pty [/project[/branch]] — PTY session diagnostics
672
789
  /history — task history
673
790
  /stop — stop listener
674
791
  /help — this help
@@ -752,6 +869,9 @@ let running = true;
752
869
  process.on('SIGTERM', () => {
753
870
  logger.info('Received SIGTERM');
754
871
  running = false;
872
+ for (const wd of liveConsoleTimers.keys()) {
873
+ stopLiveConsole(wd);
874
+ }
755
875
  runner.cancelAll();
756
876
  setTimeout(() => process.exit(0), 2000);
757
877
  });
@@ -759,6 +879,9 @@ process.on('SIGTERM', () => {
759
879
  process.on('SIGINT', () => {
760
880
  logger.info('Received SIGINT');
761
881
  running = false;
882
+ for (const wd of liveConsoleTimers.keys()) {
883
+ stopLiveConsole(wd);
884
+ }
762
885
  runner.cancelAll();
763
886
  setTimeout(() => process.exit(0), 2000);
764
887
  });
@@ -13,12 +13,13 @@ const DEFAULT_TIMEOUT = 600_000; // 10 minutes
13
13
  * receives completion signals via marker files written by the notifier hook.
14
14
  */
15
15
  export class PtyRunner extends EventEmitter {
16
- constructor (logger, timeout, taskLogger) {
16
+ constructor (logger, timeout, taskLogger, ptyLogDir) {
17
17
  super();
18
18
  this.logger = logger;
19
19
  this.timeout = timeout || DEFAULT_TIMEOUT;
20
20
  this.taskLogger = taskLogger || null;
21
- // workDir -> { pty, state, currentTask, sessionId, workDir, _pendingId, _buffer }
21
+ this.ptyLogDir = ptyLogDir || null;
22
+ // workDir -> { pty, state, currentTask, sessionId, workDir, _pendingId, _buffer, _logStream }
22
23
  this.sessions = new Map();
23
24
  this.pendingMarkers = new Map(); // pendingId -> resolve callback
24
25
  this._pty = null; // lazy-loaded node-pty module
@@ -196,6 +197,27 @@ export class PtyRunner extends EventEmitter {
196
197
  });
197
198
  }
198
199
 
200
+ _openPtyLog (session, task) {
201
+ if (session._logStream) {
202
+ session._logStream.end();
203
+ session._logStream = null;
204
+ }
205
+ if (!this.ptyLogDir) {
206
+ return;
207
+ }
208
+ try {
209
+ const project = task.project || 'unknown';
210
+ const branch = (task.branch || 'main').replace(/[/\\:*?"<>|]/g, '_');
211
+ const logFile = path.join(this.ptyLogDir, `${project}_${branch}_pty.log`);
212
+ session._logStream = fs.createWriteStream(logFile, { flags: 'w' });
213
+ session._logStream.on('error', () => {
214
+ session._logStream = null;
215
+ });
216
+ } catch {
217
+ // ignore — logging is best-effort
218
+ }
219
+ }
220
+
199
221
  /**
200
222
  * Send a task to an existing PTY session and wait for completion.
201
223
  */
@@ -205,12 +227,16 @@ export class PtyRunner extends EventEmitter {
205
227
  session.state = 'busy';
206
228
  session.currentTask = task;
207
229
  session._pendingId = pendingId;
230
+ session._buffer = '';
231
+ this._openPtyLog(session, task);
208
232
 
209
233
  // Set up marker wait + timeout
210
234
  const markerPromise = this._waitForMarker(pendingId, this.timeout);
211
235
 
212
- // Send the task text to the PTY
213
- session.pty.write(task.text + '\r');
236
+ // Send the task text to the PTY using bracketed paste mode.
237
+ // Without this, newlines in the text are interpreted as Enter keypresses,
238
+ // splitting the prompt into multiple submissions and breaking the flow.
239
+ session.pty.write(`\x1b[200~${task.text}\x1b[201~\r`);
214
240
  this.logger.info(`PTY task sent to ${workDir}: ${task.text.slice(0, 100)}`);
215
241
 
216
242
  // Handle completion asynchronously
@@ -302,6 +328,9 @@ export class PtyRunner extends EventEmitter {
302
328
  if (session._buffer.length > 50000) {
303
329
  session._buffer = session._buffer.slice(-25000);
304
330
  }
331
+ if (session._logStream) {
332
+ session._logStream.write(data);
333
+ }
305
334
  });
306
335
 
307
336
  ptyProcess.onExit(({ exitCode }) => {
@@ -380,6 +409,11 @@ export class PtyRunner extends EventEmitter {
380
409
  this.pendingMarkers.delete(session._pendingId);
381
410
  }
382
411
 
412
+ if (session._logStream) {
413
+ session._logStream.end();
414
+ session._logStream = null;
415
+ }
416
+
383
417
  try {
384
418
  if (session.pty) {
385
419
  session.pty.kill();
@@ -430,6 +464,50 @@ export class PtyRunner extends EventEmitter {
430
464
  return session?.currentTask || null;
431
465
  }
432
466
 
467
+ /**
468
+ * Get the raw PTY buffer for a workDir.
469
+ */
470
+ getBuffer (workDir) {
471
+ const session = this.sessions.get(workDir);
472
+ return session?._buffer || '';
473
+ }
474
+
475
+ /**
476
+ * Get diagnostic info about a PTY session.
477
+ */
478
+ getSessionInfo (workDir) {
479
+ const session = this.sessions.get(workDir);
480
+ if (!session) {
481
+ return null;
482
+ }
483
+ return {
484
+ state: session.state,
485
+ bufferSize: session._buffer?.length || 0,
486
+ hasLogStream: !!session._logStream,
487
+ taskText: session.currentTask?.text || null,
488
+ startedAt: session.currentTask?.startedAt || null,
489
+ sessionId: session.sessionId || null,
490
+ pendingId: session._pendingId || null,
491
+ };
492
+ }
493
+
494
+ /**
495
+ * Get diagnostic info for all sessions.
496
+ */
497
+ getAllSessionInfo () {
498
+ const result = {};
499
+ for (const [workDir, session] of this.sessions) {
500
+ result[workDir] = {
501
+ state: session.state,
502
+ bufferSize: session._buffer?.length || 0,
503
+ hasLogStream: !!session._logStream,
504
+ taskText: session.currentTask?.text || null,
505
+ startedAt: session.currentTask?.startedAt || null,
506
+ };
507
+ }
508
+ return result;
509
+ }
510
+
433
511
  /**
434
512
  * Cancel all active tasks (for graceful shutdown).
435
513
  */
@@ -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,74 @@ function splitMessage (text) {
200
237
  return chunks;
201
238
  }
202
239
 
203
- export { escapeHtml };
240
+ // Strip ANSI escape codes and terminal control sequences from PTY output
241
+ function stripAnsi (text) {
242
+ return text
243
+ // CSI sequences: \x1b[ followed by optional ?/>/! prefix, params, and terminator
244
+ .replace(/\x1b\[[?>=!]?[0-9;]*[a-zA-Z~]/g, '')
245
+ // OSC sequences: \x1b] ... (terminated by BEL or ST)
246
+ .replace(/\x1b][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
247
+ // Other two-char escape sequences (\x1b followed by any single char)
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
+ // Clean PTY output for display: strip ANSI + remove Claude Code UI chrome
256
+ function cleanPtyOutput (raw) {
257
+ const stripped = stripAnsi(raw);
258
+ const lines = stripped.split('\n');
259
+ const cleaned = [];
260
+ for (const line of lines) {
261
+ const trimmed = line.trimEnd();
262
+ // Skip empty lines
263
+ if (!trimmed) {
264
+ continue;
265
+ }
266
+ // Skip Claude Code UI: logo, banner, horizontal rules, prompts, status
267
+ if (/^[▐▝▘▛▜█▌▀▄░▒▓\s]+$/.test(trimmed)) {
268
+ continue;
269
+ }
270
+ if (/^[─━═╌┄]+$/.test(trimmed)) {
271
+ continue;
272
+ }
273
+ if (/^❯\s/.test(trimmed)) {
274
+ continue;
275
+ }
276
+ if (/^[⏵⏴]\s*[⏵⏴]?\s*(bypass|auto|plan|permissions?)/i.test(trimmed)) {
277
+ continue;
278
+ }
279
+ if (/^◐\s/.test(trimmed) || /^\s*◐\s/.test(trimmed)) {
280
+ continue;
281
+ }
282
+ if (/Pasting\s*text/i.test(trimmed)) {
283
+ continue;
284
+ }
285
+ if (/^Claude\s*Code\s*v/i.test(trimmed)) {
286
+ continue;
287
+ }
288
+ if (/Opus|Sonnet|Haiku|Claude\s*Max/i.test(trimmed) && trimmed.length < 80) {
289
+ continue;
290
+ }
291
+ if (/shift\+tab\s*to\s*cycle/i.test(trimmed)) {
292
+ continue;
293
+ }
294
+ if (/ctrl\+[a-z]\s+to\s/i.test(trimmed)) {
295
+ continue;
296
+ }
297
+ if (/^Try\s*"/.test(trimmed)) {
298
+ continue;
299
+ }
300
+ // Skip lines that are mostly box-drawing or block chars (>50%)
301
+ const specialChars = (trimmed.match(/[▐▝▘▛▜█▌▀▄░▒▓─━═╌┄│┃┌┐└┘├┤┬┴┼╔╗╚╝╠╣╦╩╬]/g) || []).length;
302
+ if (specialChars > trimmed.length * 0.5) {
303
+ continue;
304
+ }
305
+ cleaned.push(trimmed);
306
+ }
307
+ return cleaned.join('\n');
308
+ }
309
+
310
+ export { escapeHtml, stripAnsi, cleanPtyOutput };
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.39",
5
5
  "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
6
  "type": "module",
7
7
  "engines": {