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.
- package/.claude-plugin/plugin.json +9 -2
- package/README.md +7 -1
- package/commit-sha +1 -1
- package/listener/LISTENER-DETAILED.md +64 -0
- package/listener/listener.js +125 -2
- package/listener/pty-runner.js +82 -4
- package/listener/telegram-poller.js +108 -1
- 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.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": [
|
|
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
|
-
|
|
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
|
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 } 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
|
});
|
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,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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|