claude-notification-plugin 1.1.38 → 1.1.40
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -1
- package/commit-sha +1 -1
- package/listener/LISTENER-DETAILED.md +64 -0
- package/listener/listener.js +132 -13
- package/listener/pty-runner.js +70 -2
- package/listener/telegram-poller.js +102 -10
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.40",
|
|
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,9 +248,11 @@ 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
|
+
| `/menu` | Show inline button menu |
|
|
255
|
+
| `/help` | Show help with inline buttons |
|
|
254
256
|
|
|
255
257
|
### Listener configuration
|
|
256
258
|
|
|
@@ -269,6 +271,7 @@ All commands start with `/` and execute instantly (not queued).
|
|
|
269
271
|
| `liveConsole` | `true` | Stream PTY output to the "Running..." Telegram message in real-time |
|
|
270
272
|
| `liveConsoleInterval`| `5` | Live console update interval in seconds |
|
|
271
273
|
|
|
274
|
+
|
|
272
275
|
### Projects and worktrees
|
|
273
276
|
|
|
274
277
|
**The queue is tied to the working directory, not the project name:**
|
package/commit-sha
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
03d819f371c1cd76acd7127b9c1e1a6a90b4daa9
|
|
@@ -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
|
package/listener/listener.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
|
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
|
-
|
|
322
|
+
} catch (err) {
|
|
323
|
+
logger.warn(`Live console edit error: ${err.message}`);
|
|
327
324
|
}
|
|
328
325
|
}, liveConsoleInterval);
|
|
329
326
|
liveConsoleTimers.set(workDir, timer);
|
|
@@ -415,10 +412,14 @@ 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':
|
|
421
420
|
return handleHelp();
|
|
421
|
+
case '/menu':
|
|
422
|
+
return handleMenu();
|
|
422
423
|
default:
|
|
423
424
|
return `Unknown command: ${cmd}`;
|
|
424
425
|
}
|
|
@@ -433,8 +434,12 @@ function handleStatus (args) {
|
|
|
433
434
|
return `📊 Project "${target.project}": no active queues`;
|
|
434
435
|
}
|
|
435
436
|
let text = `📊 Project "<b>${escapeHtml(target.project)}</b>":\n`;
|
|
437
|
+
const buttons = [];
|
|
436
438
|
for (const s of statuses) {
|
|
437
439
|
const branchLabel = s.branch || 'main';
|
|
440
|
+
const label = s.branch && s.branch !== 'main' && s.branch !== 'master'
|
|
441
|
+
? `/${target.project}/${s.branch}`
|
|
442
|
+
: `/${target.project}`;
|
|
438
443
|
if (s.active) {
|
|
439
444
|
const elapsed = s.active.startedAt
|
|
440
445
|
? formatDuration(Date.now() - new Date(s.active.startedAt).getTime())
|
|
@@ -442,10 +447,20 @@ function handleStatus (args) {
|
|
|
442
447
|
text += `\n<b>${escapeHtml(branchLabel)}</b>:\n`;
|
|
443
448
|
text += ` ▶ ${escapeHtml(s.active.text)} (${elapsed})\n`;
|
|
444
449
|
text += ` Queue: ${s.queueLength} tasks\n`;
|
|
450
|
+
buttons.push([
|
|
451
|
+
{ text: `🛑 Cancel ${label}`, callback_data: `/cancel ${label}` },
|
|
452
|
+
{ text: `🧹 Clear ${label}`, callback_data: `/clear ${label}` },
|
|
453
|
+
]);
|
|
445
454
|
} else {
|
|
446
455
|
text += `\n<b>${escapeHtml(branchLabel)}</b>: ✅ idle\n`;
|
|
447
456
|
text += ` Queue: ${s.queueLength} tasks\n`;
|
|
448
457
|
}
|
|
458
|
+
buttons.push([
|
|
459
|
+
{ text: `🆕 New session ${label}`, callback_data: `/newsession ${label}` },
|
|
460
|
+
]);
|
|
461
|
+
}
|
|
462
|
+
if (buttons.length > 0) {
|
|
463
|
+
return { text, replyMarkup: { inline_keyboard: buttons } };
|
|
449
464
|
}
|
|
450
465
|
return text;
|
|
451
466
|
}
|
|
@@ -460,10 +475,14 @@ function handleStatus (args) {
|
|
|
460
475
|
let text = '📊 <b>Status:</b>\n';
|
|
461
476
|
const uptime = formatDuration(Date.now() - startTime);
|
|
462
477
|
text += `Uptime: ${uptime}\n`;
|
|
478
|
+
const buttons = [];
|
|
463
479
|
for (const [project, statuses] of Object.entries(all)) {
|
|
464
480
|
text += `\n<b>${escapeHtml(project)}</b>:`;
|
|
465
481
|
for (const s of statuses) {
|
|
466
482
|
const branchLabel = s.branch || 'main';
|
|
483
|
+
const label = s.branch && s.branch !== 'main' && s.branch !== 'master'
|
|
484
|
+
? `/${project}/${s.branch}`
|
|
485
|
+
: `/${project}`;
|
|
467
486
|
if (s.active) {
|
|
468
487
|
const elapsed = s.active.startedAt
|
|
469
488
|
? formatDuration(Date.now() - new Date(s.active.startedAt).getTime())
|
|
@@ -472,11 +491,21 @@ function handleStatus (args) {
|
|
|
472
491
|
if (s.queueLength > 0) {
|
|
473
492
|
text += ` +${s.queueLength} queued`;
|
|
474
493
|
}
|
|
494
|
+
buttons.push([
|
|
495
|
+
{ text: `🛑 Cancel ${label}`, callback_data: `/cancel ${label}` },
|
|
496
|
+
{ text: `🧹 Clear ${label}`, callback_data: `/clear ${label}` },
|
|
497
|
+
]);
|
|
475
498
|
} else {
|
|
476
499
|
text += `\n ${escapeHtml(branchLabel)}: ✅ idle`;
|
|
477
500
|
}
|
|
501
|
+
buttons.push([
|
|
502
|
+
{ text: `🆕 New session ${label}`, callback_data: `/newsession ${label}` },
|
|
503
|
+
]);
|
|
478
504
|
}
|
|
479
505
|
}
|
|
506
|
+
if (buttons.length > 0) {
|
|
507
|
+
return { text, replyMarkup: { inline_keyboard: buttons } };
|
|
508
|
+
}
|
|
480
509
|
return text;
|
|
481
510
|
}
|
|
482
511
|
|
|
@@ -691,6 +720,63 @@ function handleRemoveWorktree (args) {
|
|
|
691
720
|
}
|
|
692
721
|
}
|
|
693
722
|
|
|
723
|
+
function handlePty (args) {
|
|
724
|
+
const target = parseTarget(args);
|
|
725
|
+
|
|
726
|
+
if (target) {
|
|
727
|
+
let workDir;
|
|
728
|
+
try {
|
|
729
|
+
workDir = worktreeManager.resolveWorkDir(target.project, target.branch);
|
|
730
|
+
} catch (err) {
|
|
731
|
+
return `❌ ${escapeHtml(err.message)}`;
|
|
732
|
+
}
|
|
733
|
+
const info = runner.getSessionInfo(workDir);
|
|
734
|
+
if (!info) {
|
|
735
|
+
return `🖥 No PTY session for /${escapeHtml(target.project)}${target.branch ? '/' + escapeHtml(target.branch) : ''}`;
|
|
736
|
+
}
|
|
737
|
+
return formatPtyInfo(target.project, target.branch, workDir, info);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// All sessions
|
|
741
|
+
const allInfo = runner.getAllSessionInfo();
|
|
742
|
+
if (Object.keys(allInfo).length === 0) {
|
|
743
|
+
return '🖥 No active PTY sessions';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let text = '🖥 <b>PTY Sessions:</b>\n';
|
|
747
|
+
for (const [workDir, info] of Object.entries(allInfo)) {
|
|
748
|
+
const entry = queue.queues[workDir];
|
|
749
|
+
const project = entry?.project || '?';
|
|
750
|
+
const branch = entry?.branch || null;
|
|
751
|
+
text += '\n' + formatPtyInfo(project, branch, workDir, info);
|
|
752
|
+
}
|
|
753
|
+
return text;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function formatPtyInfo (project, branch, workDir, info) {
|
|
757
|
+
const label = branch && branch !== 'main' && branch !== 'master'
|
|
758
|
+
? `/${project}/${branch}`
|
|
759
|
+
: `/${project}`;
|
|
760
|
+
const elapsed = info.startedAt
|
|
761
|
+
? formatDuration(Date.now() - new Date(info.startedAt).getTime())
|
|
762
|
+
: '-';
|
|
763
|
+
const liveTimer = liveConsoleTimers.has(workDir) ? '✅' : '❌';
|
|
764
|
+
const raw = runner.getBuffer(workDir);
|
|
765
|
+
const cleaned = raw ? cleanPtyOutput(raw) : '';
|
|
766
|
+
const lastLines = cleaned
|
|
767
|
+
? cleaned.split('\n').slice(-15).join('\n')
|
|
768
|
+
: '(empty)';
|
|
769
|
+
|
|
770
|
+
return `<b>${escapeHtml(label)}</b>
|
|
771
|
+
State: <code>${info.state}</code>
|
|
772
|
+
Buffer: <code>${info.bufferSize}</code> bytes
|
|
773
|
+
Elapsed: ${elapsed}
|
|
774
|
+
Live console: ${liveTimer}
|
|
775
|
+
PTY log: <code>${info.hasLogStream ? 'writing' : 'off'}</code>
|
|
776
|
+
|
|
777
|
+
<pre>${escapeHtml(lastLines)}</pre>`;
|
|
778
|
+
}
|
|
779
|
+
|
|
694
780
|
function handleHistory () {
|
|
695
781
|
const history = queue.getHistory(10);
|
|
696
782
|
if (history.length === 0) {
|
|
@@ -715,8 +801,28 @@ async function handleStop () {
|
|
|
715
801
|
return null; // Message already sent
|
|
716
802
|
}
|
|
717
803
|
|
|
804
|
+
const MENU_KEYBOARD = {
|
|
805
|
+
inline_keyboard: [
|
|
806
|
+
[
|
|
807
|
+
{ text: '📊 Status', callback_data: '/status' },
|
|
808
|
+
{ text: '📋 Queue', callback_data: '/queue' },
|
|
809
|
+
{ text: '📂 Projects', callback_data: '/projects' },
|
|
810
|
+
],
|
|
811
|
+
[
|
|
812
|
+
{ text: '📜 History', callback_data: '/history' },
|
|
813
|
+
{ text: '🖥 PTY', callback_data: '/pty' },
|
|
814
|
+
{ text: '📖 Help', callback_data: '/help' },
|
|
815
|
+
],
|
|
816
|
+
],
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
function handleMenu () {
|
|
820
|
+
return { text: '📖 <b>Menu:</b>', replyMarkup: MENU_KEYBOARD };
|
|
821
|
+
}
|
|
822
|
+
|
|
718
823
|
function handleHelp () {
|
|
719
|
-
return
|
|
824
|
+
return {
|
|
825
|
+
text: `<b>📖 Commands:</b>
|
|
720
826
|
|
|
721
827
|
/status — status of all projects
|
|
722
828
|
/status /project — project status
|
|
@@ -729,8 +835,10 @@ function handleHelp () {
|
|
|
729
835
|
/worktrees /project — project worktrees
|
|
730
836
|
/worktree /project/branch — create worktree
|
|
731
837
|
/rmworktree /project/branch — remove worktree
|
|
838
|
+
/pty [/project[/branch]] — PTY session diagnostics
|
|
732
839
|
/history — task history
|
|
733
840
|
/stop — stop listener
|
|
841
|
+
/menu — command buttons
|
|
734
842
|
/help — this help
|
|
735
843
|
|
|
736
844
|
<b>Tasks:</b>
|
|
@@ -740,7 +848,9 @@ function handleHelp () {
|
|
|
740
848
|
|
|
741
849
|
<b>Session:</b>
|
|
742
850
|
🆕 = new session, 🔄 = continuing session
|
|
743
|
-
ctx N% = context window usage
|
|
851
|
+
ctx N% = context window usage`,
|
|
852
|
+
replyMarkup: MENU_KEYBOARD,
|
|
853
|
+
};
|
|
744
854
|
}
|
|
745
855
|
|
|
746
856
|
// ----------------------
|
|
@@ -834,6 +944,11 @@ async function mainLoop () {
|
|
|
834
944
|
try {
|
|
835
945
|
const messages = await poller.getUpdates();
|
|
836
946
|
for (const msg of messages) {
|
|
947
|
+
// Answer callback query (Telegram requires this)
|
|
948
|
+
if (msg.callbackQueryId) {
|
|
949
|
+
await poller.answerCallbackQuery(msg.callbackQueryId);
|
|
950
|
+
}
|
|
951
|
+
|
|
837
952
|
const parsed = parseMessage(msg.text);
|
|
838
953
|
if (!parsed) {
|
|
839
954
|
continue;
|
|
@@ -843,7 +958,11 @@ async function mainLoop () {
|
|
|
843
958
|
logger.info(`Command: ${parsed.cmd} ${parsed.args}`);
|
|
844
959
|
const response = await handleCommand(parsed.cmd, parsed.args);
|
|
845
960
|
if (response) {
|
|
846
|
-
|
|
961
|
+
if (typeof response === 'object' && response.text) {
|
|
962
|
+
await poller.sendMessage(response.text, msg.callbackQueryId ? null : msg.messageId, response.replyMarkup);
|
|
963
|
+
} else {
|
|
964
|
+
await poller.sendMessage(response, msg.callbackQueryId ? null : msg.messageId);
|
|
965
|
+
}
|
|
847
966
|
}
|
|
848
967
|
} else if (parsed.type === 'task') {
|
|
849
968
|
logger.info(`Task for /${parsed.project}${parsed.branch ? '/' + parsed.branch : ''}: ${parsed.text}`);
|
package/listener/pty-runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
*/
|
|
@@ -16,7 +16,7 @@ export class TelegramPoller {
|
|
|
16
16
|
|
|
17
17
|
async flush () {
|
|
18
18
|
try {
|
|
19
|
-
const url = `${this.baseUrl}/getUpdates?offset=-1&timeout=0&allowed_updates
|
|
19
|
+
const url = `${this.baseUrl}/getUpdates?offset=-1&timeout=0&allowed_updates=${encodeURIComponent('["message","callback_query"]')}`;
|
|
20
20
|
const res = await fetch(url, { signal: AbortSignal.timeout(10000) });
|
|
21
21
|
const data = await res.json();
|
|
22
22
|
if (data.ok && data.result?.length) {
|
|
@@ -34,7 +34,7 @@ export class TelegramPoller {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
try {
|
|
37
|
-
const url = `${this.baseUrl}/getUpdates?offset=${this.offset}&timeout=${POLL_TIMEOUT}&allowed_updates
|
|
37
|
+
const url = `${this.baseUrl}/getUpdates?offset=${this.offset}&timeout=${POLL_TIMEOUT}&allowed_updates=${encodeURIComponent('["message","callback_query"]')}`;
|
|
38
38
|
const res = await fetch(url, { signal: AbortSignal.timeout((POLL_TIMEOUT + 10) * 1000) });
|
|
39
39
|
const data = await res.json();
|
|
40
40
|
if (!data.ok) {
|
|
@@ -52,6 +52,23 @@ export class TelegramPoller {
|
|
|
52
52
|
const messages = [];
|
|
53
53
|
for (const update of data.result || []) {
|
|
54
54
|
this.offset = update.update_id + 1;
|
|
55
|
+
|
|
56
|
+
// Handle callback_query (inline button press)
|
|
57
|
+
const cb = update.callback_query;
|
|
58
|
+
if (cb) {
|
|
59
|
+
if (String(cb.message?.chat?.id) !== this.chatId) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
messages.push({
|
|
63
|
+
messageId: cb.message.message_id,
|
|
64
|
+
text: cb.data,
|
|
65
|
+
chatId: cb.message.chat.id,
|
|
66
|
+
date: cb.message.date,
|
|
67
|
+
callbackQueryId: cb.id,
|
|
68
|
+
});
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
const msg = update.message;
|
|
56
73
|
if (!msg || !msg.text) {
|
|
57
74
|
continue;
|
|
@@ -83,7 +100,7 @@ export class TelegramPoller {
|
|
|
83
100
|
this._errorBackoff = Math.min(1000 * Math.pow(2, this._consecutiveErrors - 1), 30000);
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
async sendMessage (text, replyToMessageId) {
|
|
103
|
+
async sendMessage (text, replyToMessageId, replyMarkup) {
|
|
87
104
|
const chunks = splitMessage(text);
|
|
88
105
|
let firstMessageId = null;
|
|
89
106
|
for (const chunk of chunks) {
|
|
@@ -96,6 +113,10 @@ export class TelegramPoller {
|
|
|
96
113
|
if (replyToMessageId) {
|
|
97
114
|
body.reply_to_message_id = replyToMessageId;
|
|
98
115
|
}
|
|
116
|
+
// Attach inline keyboard only to the last chunk
|
|
117
|
+
if (replyMarkup && chunk === chunks[chunks.length - 1]) {
|
|
118
|
+
body.reply_markup = replyMarkup;
|
|
119
|
+
}
|
|
99
120
|
const res = await fetch(`${this.baseUrl}/sendMessage`, {
|
|
100
121
|
method: 'POST',
|
|
101
122
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -127,6 +148,22 @@ export class TelegramPoller {
|
|
|
127
148
|
return firstMessageId;
|
|
128
149
|
}
|
|
129
150
|
|
|
151
|
+
async answerCallbackQuery (callbackQueryId, text) {
|
|
152
|
+
try {
|
|
153
|
+
const body = { callback_query_id: callbackQueryId };
|
|
154
|
+
if (text) {
|
|
155
|
+
body.text = text;
|
|
156
|
+
}
|
|
157
|
+
await fetch(`${this.baseUrl}/answerCallbackQuery`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: JSON.stringify(body),
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
this.logger.error(`answerCallbackQuery error: ${err.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
130
167
|
async deleteMessage (messageId) {
|
|
131
168
|
if (!messageId) {
|
|
132
169
|
return;
|
|
@@ -237,14 +274,14 @@ function splitMessage (text) {
|
|
|
237
274
|
return chunks;
|
|
238
275
|
}
|
|
239
276
|
|
|
240
|
-
// Strip ANSI escape codes and
|
|
277
|
+
// Strip ANSI escape codes and terminal control sequences from PTY output
|
|
241
278
|
function stripAnsi (text) {
|
|
242
279
|
return text
|
|
243
|
-
//
|
|
244
|
-
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
245
|
-
// OSC sequences (
|
|
246
|
-
.replace(/\x1b
|
|
247
|
-
// Other escape sequences
|
|
280
|
+
// CSI sequences: \x1b[ followed by optional ?/>/! prefix, params, and terminator
|
|
281
|
+
.replace(/\x1b\[[?>=!]?[0-9;]*[a-zA-Z~]/g, '')
|
|
282
|
+
// OSC sequences: \x1b] ... (terminated by BEL or ST)
|
|
283
|
+
.replace(/\x1b][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
284
|
+
// Other two-char escape sequences (\x1b followed by any single char)
|
|
248
285
|
.replace(/\x1b[^[\]]/g, '')
|
|
249
286
|
// Carriage returns (overwrite lines)
|
|
250
287
|
.replace(/\r/g, '')
|
|
@@ -252,4 +289,59 @@ function stripAnsi (text) {
|
|
|
252
289
|
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
|
|
253
290
|
}
|
|
254
291
|
|
|
255
|
-
|
|
292
|
+
// Clean PTY output for display: strip ANSI + remove Claude Code UI chrome
|
|
293
|
+
function cleanPtyOutput (raw) {
|
|
294
|
+
const stripped = stripAnsi(raw);
|
|
295
|
+
const lines = stripped.split('\n');
|
|
296
|
+
const cleaned = [];
|
|
297
|
+
for (const line of lines) {
|
|
298
|
+
const trimmed = line.trimEnd();
|
|
299
|
+
// Skip empty lines
|
|
300
|
+
if (!trimmed) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// Skip Claude Code UI: logo, banner, horizontal rules, prompts, status
|
|
304
|
+
if (/^[▐▝▘▛▜█▌▀▄░▒▓\s]+$/.test(trimmed)) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (/^[─━═╌┄]+$/.test(trimmed)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (/^❯\s/.test(trimmed)) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (/^[⏵⏴]\s*[⏵⏴]?\s*(bypass|auto|plan|permissions?)/i.test(trimmed)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (/^◐\s/.test(trimmed) || /^\s*◐\s/.test(trimmed)) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (/Pasting\s*text/i.test(trimmed)) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (/^Claude\s*Code\s*v/i.test(trimmed)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (/Opus|Sonnet|Haiku|Claude\s*Max/i.test(trimmed) && trimmed.length < 80) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (/shift\+tab\s*to\s*cycle/i.test(trimmed)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (/ctrl\+[a-z]\s+to\s/i.test(trimmed)) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (/^Try\s*"/.test(trimmed)) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
// Skip lines that are mostly box-drawing or block chars (>50%)
|
|
338
|
+
const specialChars = (trimmed.match(/[▐▝▘▛▜█▌▀▄░▒▓─━═╌┄│┃┌┐└┘├┤┬┴┼╔╗╚╝╠╣╦╩╬]/g) || []).length;
|
|
339
|
+
if (specialChars > trimmed.length * 0.5) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
cleaned.push(trimmed);
|
|
343
|
+
}
|
|
344
|
+
return cleaned.join('\n');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
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.
|
|
4
|
+
"version": "1.1.40",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|