claude-notification-plugin 1.1.103 → 1.1.105
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/commit-sha +1 -1
- package/listener/listener.js +12 -5
- package/listener/pty-runner.js +33 -22
- package/listener/screen-renderer.js +41 -0
- package/listener/telegram-poller.js +51 -1
- package/package.json +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.105",
|
|
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/commit-sha
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
172c80dbeb3e3f935e6e96b28d23b7ee40ba8858
|
package/listener/listener.js
CHANGED
|
@@ -298,7 +298,7 @@ async function notifyTaskCompletion (workDir, task, kind, payload = {}) {
|
|
|
298
298
|
runner.cleanActivitySignal(workDir);
|
|
299
299
|
const entry = queue.queues[workDir];
|
|
300
300
|
const label = formatLabel(entry?.project, entry?.branch);
|
|
301
|
-
|
|
301
|
+
let output = payload.text || '';
|
|
302
302
|
|
|
303
303
|
// Build header
|
|
304
304
|
let header;
|
|
@@ -311,12 +311,19 @@ async function notifyTaskCompletion (workDir, task, kind, payload = {}) {
|
|
|
311
311
|
header = `⏰ <code>${label}</code>\nTask forcefully stopped — ${reason}`;
|
|
312
312
|
queueResult = 'TIMEOUT';
|
|
313
313
|
} else if (task.raw) {
|
|
314
|
-
|
|
315
|
-
|
|
314
|
+
const rawCmd = (task.text || '').trim().toLowerCase();
|
|
315
|
+
// /clear wipes Claude's context — reset our counters to match. The PTY
|
|
316
|
+
// buffer after /clear is just an empty prompt + status bar, so skip the
|
|
317
|
+
// body dump and report a clean confirmation instead.
|
|
318
|
+
if (rawCmd === '/clear') {
|
|
316
319
|
sessions.delete(workDir);
|
|
320
|
+
header = `🧹 <code>${label}</code> <code>/clear</code> — session reset`;
|
|
321
|
+
queueResult = '/clear';
|
|
322
|
+
output = '';
|
|
323
|
+
} else {
|
|
324
|
+
header = `📨 <code>${label}</code> sent <code>${escapeHtml(task.text)}</code>`;
|
|
325
|
+
queueResult = output;
|
|
317
326
|
}
|
|
318
|
-
header = `📨 <code>${label}</code> sent <code>${escapeHtml(task.text)}</code>`;
|
|
319
|
-
queueResult = output;
|
|
320
327
|
} else {
|
|
321
328
|
// Update session tracking for non-raw completions
|
|
322
329
|
const session = sessions.get(workDir) || { taskCount: 0 };
|
package/listener/pty-runner.js
CHANGED
|
@@ -4,7 +4,8 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { EventEmitter } from 'events';
|
|
6
6
|
import { PTY_SIGNAL_DIR } from '../bin/constants.js';
|
|
7
|
-
import {
|
|
7
|
+
import { cleanRenderedScreen } from './telegram-poller.js';
|
|
8
|
+
import { renderPtyScreen } from './screen-renderer.js';
|
|
8
9
|
|
|
9
10
|
const DEFAULT_TIMEOUT = 600_000; // 10 minutes
|
|
10
11
|
// Built-in slash-commands (forwarded via %cmd) rarely emit a Stop hook event,
|
|
@@ -253,31 +254,37 @@ export class PtyRunner extends EventEmitter {
|
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
const CHECK_INTERVAL = 5000;
|
|
256
|
-
|
|
257
|
+
let idleCheckInFlight = false;
|
|
258
|
+
const checker = setInterval(async () => {
|
|
257
259
|
const lastActivity = session?._lastActivityTime || 0;
|
|
258
260
|
const idleMs = lastActivity > 0 ? Date.now() - lastActivity : 0;
|
|
259
261
|
|
|
260
262
|
// Idle-prompt fallback: PTY went quiet AND its tail shows Claude's
|
|
261
263
|
// idle status bar — treat as completed even if the Stop hook never
|
|
262
264
|
// produced a signal (happens with resumed sessions on Windows ConPTY).
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
265
|
+
// Rendering is async (xterm-headless), so guard against re-entry while
|
|
266
|
+
// an earlier tick is still rendering.
|
|
267
|
+
if (idleMs > IDLE_PROMPT_FALLBACK_MS && session && !idleCheckInFlight) {
|
|
268
|
+
idleCheckInFlight = true;
|
|
269
|
+
try {
|
|
270
|
+
const rendered = await renderPtyScreen(session._buffer || '');
|
|
271
|
+
if (IDLE_PROMPT_RE.test(rendered)) {
|
|
272
|
+
clearInterval(checker);
|
|
273
|
+
this.pendingMarkers.delete(pendingId);
|
|
274
|
+
const text = cleanRenderedScreen(rendered).trim().slice(-2000);
|
|
275
|
+
this.logger.warn(`PTY idle-prompt fallback completion in ${session.workDir} (no Stop signal in ${Math.round(idleMs / 1000)}s)`);
|
|
276
|
+
resolve({
|
|
277
|
+
lastAssistantMessage: text,
|
|
278
|
+
sessionId: session.sessionId || null,
|
|
279
|
+
cost: 0,
|
|
280
|
+
numTurns: 0,
|
|
281
|
+
durationMs: idleMs,
|
|
282
|
+
isIdleFallback: true,
|
|
283
|
+
});
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
} finally {
|
|
287
|
+
idleCheckInFlight = false;
|
|
281
288
|
}
|
|
282
289
|
}
|
|
283
290
|
|
|
@@ -437,7 +444,7 @@ export class PtyRunner extends EventEmitter {
|
|
|
437
444
|
this.taskLogger.logAnswer(task.project || 'unknown', task.branch || 'main', result.text, 0);
|
|
438
445
|
}
|
|
439
446
|
this.emit('complete', workDir, task, result);
|
|
440
|
-
}).catch((err) => {
|
|
447
|
+
}).catch(async (err) => {
|
|
441
448
|
session.state = 'idle';
|
|
442
449
|
session.currentTask = null;
|
|
443
450
|
|
|
@@ -445,7 +452,11 @@ export class PtyRunner extends EventEmitter {
|
|
|
445
452
|
if (task.raw) {
|
|
446
453
|
// Slash commands (e.g. /clear, /cost) usually don't emit a Stop hook.
|
|
447
454
|
// Treat inactivity as successful completion and keep the PTY alive.
|
|
448
|
-
|
|
455
|
+
// Render through xterm-headless so the *final* screen is what we
|
|
456
|
+
// report — naive ANSI stripping leaves transient popup rows in the
|
|
457
|
+
// output even though the real terminal has redrawn over them.
|
|
458
|
+
const rendered = await renderPtyScreen(session._buffer || '');
|
|
459
|
+
const cleaned = cleanRenderedScreen(rendered).trim();
|
|
449
460
|
const tail = cleaned.length > 2000 ? cleaned.slice(-2000) : cleaned;
|
|
450
461
|
const result = {
|
|
451
462
|
text: tail,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import xtermPkg from '@xterm/headless';
|
|
2
|
+
|
|
3
|
+
const { Terminal } = xtermPkg;
|
|
4
|
+
|
|
5
|
+
// Must match the dimensions PTY sessions are spawned with (pty-runner.js).
|
|
6
|
+
// Mismatch produces wrap artifacts in the rendered viewport.
|
|
7
|
+
const COLS = 120;
|
|
8
|
+
const ROWS = 40;
|
|
9
|
+
|
|
10
|
+
// Run raw PTY bytes through a headless xterm.js so the *final* visible screen
|
|
11
|
+
// is what we read out — cursor positioning, line erases, scroll-up, etc. are
|
|
12
|
+
// all honored. Without this, transient UI (e.g. the slash-command popup that
|
|
13
|
+
// briefly appears while typing `/clear`) leaks into the captured output even
|
|
14
|
+
// though the real terminal has long since cleared those rows.
|
|
15
|
+
//
|
|
16
|
+
// Returns the viewport text (ROWS lines joined by \n) with trailing empty
|
|
17
|
+
// lines trimmed. Scrollback is intentionally ignored — Telegram messages
|
|
18
|
+
// reflect the on-screen state, not the entire session history.
|
|
19
|
+
export async function renderPtyScreen (raw) {
|
|
20
|
+
if (!raw) {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
const term = new Terminal({ cols: COLS, rows: ROWS, allowProposedApi: true });
|
|
24
|
+
await new Promise((resolve) => term.write(raw, resolve));
|
|
25
|
+
const buf = term.buffer.active;
|
|
26
|
+
const lines = [];
|
|
27
|
+
const startY = buf.viewportY;
|
|
28
|
+
for (let y = 0; y < ROWS; y++) {
|
|
29
|
+
const line = buf.getLine(startY + y);
|
|
30
|
+
lines.push(line ? line.translateToString(true) : '');
|
|
31
|
+
}
|
|
32
|
+
while (lines.length && !lines[lines.length - 1].trim()) {
|
|
33
|
+
lines.pop();
|
|
34
|
+
}
|
|
35
|
+
if (typeof term.dispose === 'function') {
|
|
36
|
+
term.dispose();
|
|
37
|
+
}
|
|
38
|
+
return lines.join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { COLS as RENDER_COLS, ROWS as RENDER_ROWS };
|
|
@@ -416,4 +416,54 @@ function cleanPtyOutput (raw) {
|
|
|
416
416
|
return cleaned.join('\n');
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
-
|
|
419
|
+
// Softer cleaner for *rendered* terminal screens (post xterm-headless emulation).
|
|
420
|
+
// Unlike cleanPtyOutput, this preserves `❯ <command>` lines because the
|
|
421
|
+
// rendered screen already reflects the final state — that prompt line is the
|
|
422
|
+
// actual command echo + result, not a transient input being typed.
|
|
423
|
+
function cleanRenderedScreen (rendered) {
|
|
424
|
+
if (!rendered) {
|
|
425
|
+
return '';
|
|
426
|
+
}
|
|
427
|
+
const lines = rendered.split('\n');
|
|
428
|
+
const cleaned = [];
|
|
429
|
+
for (const line of lines) {
|
|
430
|
+
const trimmed = line.trimEnd();
|
|
431
|
+
if (!trimmed.trim()) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
// Empty prompt arrow (no command on it) — drop
|
|
435
|
+
if (/^❯\s*$/.test(trimmed)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
// Pure divider lines (≥50% box-drawing chars)
|
|
439
|
+
const specialChars = (trimmed.match(/[▐▝▘▛▜█▌▀▄░▒▓─━═╌┄│┃┌┐└┘├┤┬┴┼╔╗╚╝╠╣╦╩╬]/g) || []).length;
|
|
440
|
+
if (specialChars > trimmed.length * 0.5) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
// Status bar — bypass/auto/plan permissions
|
|
444
|
+
if (/[⏵⏴]\s*[⏵⏴]?\s*(bypass|auto|plan)\s+permissions?/i.test(trimmed)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (/shift\+tab\s*to\s*cycle/i.test(trimmed)) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (/ctrl\+[a-z]\s+to\s/i.test(trimmed)) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
// Effort indicator: "◉ xhigh · /effort"
|
|
454
|
+
if (/^\s*◉\s+\S+\s*·\s*\/effort\s*$/.test(trimmed)) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
// Help hint: "/ide for WebStorm" etc. at far-right column
|
|
458
|
+
if (/^\s+\/(ide|model|help)\s+(for|to)\s+/i.test(trimmed)) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (/Pasting\s*text/i.test(trimmed)) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
cleaned.push(trimmed);
|
|
465
|
+
}
|
|
466
|
+
return cleaned.join('\n');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export { escapeHtml, stripAnsi, cleanPtyOutput, cleanRenderedScreen };
|
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.105",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"@xterm/headless": "^6.0.0",
|
|
58
59
|
"node-notifier": "^10.0.1",
|
|
59
60
|
"node-pty": "^1.1.0"
|
|
60
61
|
},
|