claude-notification-plugin 1.1.38 → 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.38",
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",
package/README.md CHANGED
@@ -248,6 +248,7 @@ All commands start with `/` and execute instantly (not queued).
248
248
  | `/worktrees /project` | List worktrees |
249
249
  | `/worktree /project/branch` | Create a worktree |
250
250
  | `/rmworktree /project/branch` | Remove a worktree |
251
+ | `/pty [/project[/branch]]` | PTY session diagnostics (state, buffer, output) |
251
252
  | `/history` | Recent task history |
252
253
  | `/stop` | Stop the listener |
253
254
  | `/help` | Show help |
@@ -269,6 +270,7 @@ All commands start with `/` and execute instantly (not queued).
269
270
  | `liveConsole` | `true` | Stream PTY output to the "Running..." Telegram message in real-time |
270
271
  | `liveConsoleInterval`| `5` | Live console update interval in seconds |
271
272
 
273
+
272
274
  ### Projects and worktrees
273
275
 
274
276
  **The queue is tied to the working directory, not the project name:**
package/commit-sha CHANGED
@@ -1 +1 @@
1
- 0fdb0002b24921b5b1c581fcd13720f2bd1247e0
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, stripAnsi } 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,7 +100,7 @@ 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
 
@@ -300,11 +300,7 @@ function startLiveConsole (workDir, messageId, header) {
300
300
  if (!raw) {
301
301
  return;
302
302
  }
303
- const cleaned = stripAnsi(raw)
304
- .split('\n')
305
- .map((l) => l.trimEnd())
306
- .filter((l) => l.length > 0)
307
- .join('\n');
303
+ const cleaned = cleanPtyOutput(raw);
308
304
  if (!cleaned) {
309
305
  return;
310
306
  }
@@ -320,10 +316,11 @@ function startLiveConsole (workDir, messageId, header) {
320
316
  return;
321
317
  }
322
318
  lastSentText = output;
323
- const text = `${header}\n\n<pre>${escapeHtml(output)}</pre>`;
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>`;
324
321
  await poller.editMessage(messageId, text);
325
- } catch {
326
- // ignore edit errors message may have been deleted
322
+ } catch (err) {
323
+ logger.warn(`Live console edit error: ${err.message}`);
327
324
  }
328
325
  }, liveConsoleInterval);
329
326
  liveConsoleTimers.set(workDir, timer);
@@ -415,6 +412,8 @@ async function handleCommand (cmd, args) {
415
412
  return handleRemoveWorktree(args);
416
413
  case '/history':
417
414
  return handleHistory();
415
+ case '/pty':
416
+ return handlePty(args);
418
417
  case '/stop':
419
418
  return handleStop();
420
419
  case '/help':
@@ -691,6 +690,63 @@ function handleRemoveWorktree (args) {
691
690
  }
692
691
  }
693
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
+
694
750
  function handleHistory () {
695
751
  const history = queue.getHistory(10);
696
752
  if (history.length === 0) {
@@ -729,6 +785,7 @@ function handleHelp () {
729
785
  /worktrees /project — project worktrees
730
786
  /worktree /project/branch — create worktree
731
787
  /rmworktree /project/branch — remove worktree
788
+ /pty [/project[/branch]] — PTY session diagnostics
732
789
  /history — task history
733
790
  /stop — stop listener
734
791
  /help — this help
@@ -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,6 +227,8 @@ 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);
@@ -304,6 +328,9 @@ export class PtyRunner extends EventEmitter {
304
328
  if (session._buffer.length > 50000) {
305
329
  session._buffer = session._buffer.slice(-25000);
306
330
  }
331
+ if (session._logStream) {
332
+ session._logStream.write(data);
333
+ }
307
334
  });
308
335
 
309
336
  ptyProcess.onExit(({ exitCode }) => {
@@ -382,6 +409,11 @@ export class PtyRunner extends EventEmitter {
382
409
  this.pendingMarkers.delete(session._pendingId);
383
410
  }
384
411
 
412
+ if (session._logStream) {
413
+ session._logStream.end();
414
+ session._logStream = null;
415
+ }
416
+
385
417
  try {
386
418
  if (session.pty) {
387
419
  session.pty.kill();
@@ -440,6 +472,42 @@ export class PtyRunner extends EventEmitter {
440
472
  return session?._buffer || '';
441
473
  }
442
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
+
443
511
  /**
444
512
  * Cancel all active tasks (for graceful shutdown).
445
513
  */
@@ -237,14 +237,14 @@ function splitMessage (text) {
237
237
  return chunks;
238
238
  }
239
239
 
240
- // Strip ANSI escape codes and common terminal control sequences from PTY output
240
+ // Strip ANSI escape codes and terminal control sequences from PTY output
241
241
  function stripAnsi (text) {
242
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
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
248
  .replace(/\x1b[^[\]]/g, '')
249
249
  // Carriage returns (overwrite lines)
250
250
  .replace(/\r/g, '')
@@ -252,4 +252,59 @@ function stripAnsi (text) {
252
252
  .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
253
253
  }
254
254
 
255
- export { escapeHtml, stripAnsi };
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.38",
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": {