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