claude-notification-plugin 1.1.81 → 1.1.84
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 +21 -0
- package/commit-sha +1 -1
- package/listener/listener.js +52 -1
- package/listener/message-parser.js +29 -3
- package/listener/pty-runner.js +36 -5
- package/listener/work-queue.js +2 -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.84",
|
|
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
|
@@ -265,6 +265,27 @@ Projects are referenced with the `&` prefix (e.g. `&api`, `&api/branch`).
|
|
|
265
265
|
| `/menu` | Show help with inline buttons |
|
|
266
266
|
| `/help` | Show help with inline buttons |
|
|
267
267
|
|
|
268
|
+
#### Raw REPL commands (`%cmd`)
|
|
269
|
+
|
|
270
|
+
To forward a slash-command straight into the live Claude Code REPL (instead of
|
|
271
|
+
letting the listener intercept it), prefix it with `%`:
|
|
272
|
+
|
|
273
|
+
| Telegram message | What gets sent to Claude PTY |
|
|
274
|
+
|-------------------------|----------------------------------------------|
|
|
275
|
+
| `%clear` | `/clear` in the default project |
|
|
276
|
+
| `&api %compact` | `/compact` in the `&api` main worktree |
|
|
277
|
+
| `&api/feature %cost` | `/cost` in the `&api/feature` worktree |
|
|
278
|
+
| `%%foo` | literal task starting with `%foo` (escape) |
|
|
279
|
+
|
|
280
|
+
Useful for `/clear`, `/compact`, `/cost`, `/model`, `/status` etc. — commands
|
|
281
|
+
that act on the running Claude session without going through the listener's
|
|
282
|
+
command router. The PTY session is **kept alive**, so subsequent tasks
|
|
283
|
+
continue in the same conversation (with cleared context after `/clear`).
|
|
284
|
+
|
|
285
|
+
Because REPL commands usually don't produce a `Stop` hook event, raw tasks
|
|
286
|
+
complete after ~8 s of PTY inactivity; the last chunk of output is included
|
|
287
|
+
in the confirmation reply.
|
|
288
|
+
|
|
268
289
|
#### Registering projects on the fly (`/addproject` + `/seen`)
|
|
269
290
|
|
|
270
291
|
Whenever the notifier fires for a folder, it records the absolute path,
|
package/commit-sha
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
642d44de740e6e85d391634789966980a389d893
|
package/listener/listener.js
CHANGED
|
@@ -201,6 +201,29 @@ runner.on('complete', async (workDir, task, result) => {
|
|
|
201
201
|
// Delete the "Running" message
|
|
202
202
|
await poller.deleteMessage(task.runningMessageId);
|
|
203
203
|
|
|
204
|
+
const output = result.text || '';
|
|
205
|
+
|
|
206
|
+
if (task.raw) {
|
|
207
|
+
// Raw slash-command: compact "sent" confirmation, don't bump session counter.
|
|
208
|
+
// `/clear` wipes Claude's context — reset our counters too.
|
|
209
|
+
const normalized = (task.text || '').trim().toLowerCase();
|
|
210
|
+
if (normalized === '/clear') {
|
|
211
|
+
sessions.delete(workDir);
|
|
212
|
+
}
|
|
213
|
+
const headerShort = `📨 <code>${label}</code> sent <code>${escapeHtml(task.text)}</code>`;
|
|
214
|
+
const tail = output ? output.slice(-1500) : '';
|
|
215
|
+
const body = tail ? `\n\n<pre>${escapeHtml(tail)}</pre>` : '';
|
|
216
|
+
const sentId = await poller.sendMessage(headerShort + body, task.telegramMessageId);
|
|
217
|
+
if (!sentId && task.telegramMessageId) {
|
|
218
|
+
await poller.sendMessage(headerShort + body);
|
|
219
|
+
}
|
|
220
|
+
const next = queue.onTaskComplete(workDir, output);
|
|
221
|
+
if (next) {
|
|
222
|
+
startTask(workDir, next);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
204
227
|
// Update session tracking
|
|
205
228
|
const session = sessions.get(workDir) || { taskCount: 0 };
|
|
206
229
|
session.taskCount++;
|
|
@@ -231,7 +254,6 @@ runner.on('complete', async (workDir, task, result) => {
|
|
|
231
254
|
const sessionIcon = task.continueSession ? '🔄' : '🆕';
|
|
232
255
|
|
|
233
256
|
// Build result
|
|
234
|
-
const output = result.text || '';
|
|
235
257
|
const headerShort = `✅ ${sessionIcon} <code>${label}</code>${sessionInfo}`;
|
|
236
258
|
const headerFull = `${headerShort}\n\n${escapeHtml(task.text)}`;
|
|
237
259
|
let body = '';
|
|
@@ -429,6 +451,29 @@ async function startTask (workDir, task) {
|
|
|
429
451
|
const continueSession = shouldContinueSession(workDir);
|
|
430
452
|
const session = sessions.get(workDir);
|
|
431
453
|
|
|
454
|
+
// Raw slash-commands get a compact running message and skip the live console.
|
|
455
|
+
if (task.raw) {
|
|
456
|
+
const runningRaw = `📨 <code>${label}</code> sending <code>${escapeHtml(task.text)}</code>…`;
|
|
457
|
+
let runningMsgId = null;
|
|
458
|
+
if (task.telegramMessageId) {
|
|
459
|
+
runningMsgId = await poller.sendMessage(runningRaw, task.telegramMessageId);
|
|
460
|
+
}
|
|
461
|
+
if (!runningMsgId) {
|
|
462
|
+
runningMsgId = await poller.sendMessage(runningRaw);
|
|
463
|
+
}
|
|
464
|
+
task.runningMessageId = runningMsgId;
|
|
465
|
+
const claudeArgs = getClaudeArgs(entry?.project);
|
|
466
|
+
try {
|
|
467
|
+
runner.run(workDir, task, claudeArgs, continueSession);
|
|
468
|
+
queue.markStarted(workDir, task.pid || 0);
|
|
469
|
+
} catch (err) {
|
|
470
|
+
logger.error(`Failed to start raw task: ${err.message}`);
|
|
471
|
+
poller.sendMessage(`❌ <code>${label}</code>\nFailed to start: ${escapeHtml(err.message)}`);
|
|
472
|
+
queue.onTaskComplete(workDir, `START_ERROR: ${err.message}`);
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
432
477
|
// Build running message with session info
|
|
433
478
|
let sessionTag = '';
|
|
434
479
|
if (continueSession && session) {
|
|
@@ -1239,6 +1284,11 @@ function handleHelp () {
|
|
|
1239
1284
|
<code>&project/branch task</code> — worktree
|
|
1240
1285
|
<code>task</code> — default project
|
|
1241
1286
|
|
|
1287
|
+
<b>Raw REPL commands (forward to live Claude session):</b>
|
|
1288
|
+
<code>%clear</code> — send <code>/clear</code> into the running Claude PTY
|
|
1289
|
+
<code>&project %compact</code> — same, targeting project
|
|
1290
|
+
<code>%%foo</code> — literal task starting with <code>%foo</code> (escape)
|
|
1291
|
+
|
|
1242
1292
|
<b>Session:</b>
|
|
1243
1293
|
🆕 = new session, 🔄 = continuing session
|
|
1244
1294
|
ctx N% = context window usage`,
|
|
@@ -1286,6 +1336,7 @@ async function handleTask (parsed, telegramMessageId) {
|
|
|
1286
1336
|
parsed.branch || 'main',
|
|
1287
1337
|
parsed.text,
|
|
1288
1338
|
telegramMessageId,
|
|
1339
|
+
!!parsed.raw,
|
|
1289
1340
|
);
|
|
1290
1341
|
|
|
1291
1342
|
if (result.error) {
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
* &project text → { type: 'task', project, branch: null, text }
|
|
10
10
|
* text → { type: 'task', project: <defaultProject>, branch: null, text }
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Raw slash-commands forwarded to the live Claude REPL:
|
|
13
|
+
* %cmd [args] → task with raw:true, text='/cmd [args]'
|
|
14
|
+
* &project %cmd [args] → same, targeting the project's PTY
|
|
15
|
+
* %%foo → literal task starting with "%foo" (escape)
|
|
16
|
+
*
|
|
17
|
+
* Any /word is treated as a listener command (known or unknown).
|
|
13
18
|
* Project designation uses & prefix: &project or &project/branch.
|
|
14
19
|
*
|
|
15
20
|
* @param {string} text - The message text.
|
|
@@ -41,7 +46,8 @@ export function parseMessage (text, defaultProject) {
|
|
|
41
46
|
const projectMatch = trimmed.match(/^&(\S+)\s+([\s\S]+)$/);
|
|
42
47
|
if (projectMatch) {
|
|
43
48
|
const target = projectMatch[1];
|
|
44
|
-
const
|
|
49
|
+
const rawText = projectMatch[2].trim();
|
|
50
|
+
const { text: taskText, raw } = parseRawPrefix(rawText);
|
|
45
51
|
|
|
46
52
|
const slashIndex = target.indexOf('/');
|
|
47
53
|
if (slashIndex > 0) {
|
|
@@ -50,6 +56,7 @@ export function parseMessage (text, defaultProject) {
|
|
|
50
56
|
project: target.substring(0, slashIndex),
|
|
51
57
|
branch: target.substring(slashIndex + 1),
|
|
52
58
|
text: taskText,
|
|
59
|
+
raw,
|
|
53
60
|
};
|
|
54
61
|
}
|
|
55
62
|
return {
|
|
@@ -57,19 +64,38 @@ export function parseMessage (text, defaultProject) {
|
|
|
57
64
|
project: target,
|
|
58
65
|
branch: null,
|
|
59
66
|
text: taskText,
|
|
67
|
+
raw,
|
|
60
68
|
};
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
// Plain text → default project
|
|
73
|
+
const { text: taskText, raw } = parseRawPrefix(trimmed);
|
|
65
74
|
return {
|
|
66
75
|
type: 'task',
|
|
67
76
|
project: defaultProject || 'default',
|
|
68
77
|
branch: null,
|
|
69
|
-
text:
|
|
78
|
+
text: taskText,
|
|
79
|
+
raw,
|
|
70
80
|
};
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Detect `%cmd` / `%%literal` prefix.
|
|
85
|
+
* - `%%foo` → plain task "%foo"
|
|
86
|
+
* - `%foo` → raw task "/foo" (forwarded to PTY verbatim)
|
|
87
|
+
* - anything else → unchanged plain task
|
|
88
|
+
*/
|
|
89
|
+
function parseRawPrefix (text) {
|
|
90
|
+
if (text.startsWith('%%')) {
|
|
91
|
+
return { text: text.slice(1), raw: false };
|
|
92
|
+
}
|
|
93
|
+
if (text.startsWith('%')) {
|
|
94
|
+
return { text: '/' + text.slice(1), raw: true };
|
|
95
|
+
}
|
|
96
|
+
return { text, raw: false };
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
/**
|
|
74
100
|
* Parse &project or &project/branch from command args.
|
|
75
101
|
* Returns { project, branch, rest } or null.
|
package/listener/pty-runner.js
CHANGED
|
@@ -4,8 +4,12 @@ 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 { cleanPtyOutput } from './telegram-poller.js';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_TIMEOUT = 600_000; // 10 minutes
|
|
10
|
+
// Built-in slash-commands (forwarded via %cmd) rarely emit a Stop hook event,
|
|
11
|
+
// so we fall back to "done" after this much buffer inactivity.
|
|
12
|
+
const RAW_INACTIVITY_MS = 8_000;
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* PTY-based runner for Claude Code.
|
|
@@ -318,8 +322,11 @@ export class PtyRunner extends EventEmitter {
|
|
|
318
322
|
session._buffer = '';
|
|
319
323
|
this._openPtyLog(session, task);
|
|
320
324
|
|
|
321
|
-
// Set up marker wait + inactivity timeout
|
|
322
|
-
|
|
325
|
+
// Set up marker wait + inactivity timeout. Raw slash-commands use a much
|
|
326
|
+
// shorter window because they typically don't trigger an agent turn and
|
|
327
|
+
// therefore never produce a Stop signal.
|
|
328
|
+
const inactivityMs = task.raw ? RAW_INACTIVITY_MS : this.timeout;
|
|
329
|
+
const markerPromise = this._waitForMarker(pendingId, inactivityMs, session);
|
|
323
330
|
|
|
324
331
|
// Send the task text to the PTY.
|
|
325
332
|
// Bracketed paste mode (\x1b[200~...\x1b[201~) causes Claude to hang in ConPTY,
|
|
@@ -379,6 +386,29 @@ export class PtyRunner extends EventEmitter {
|
|
|
379
386
|
session.currentTask = null;
|
|
380
387
|
|
|
381
388
|
if (err.message === 'Marker timeout') {
|
|
389
|
+
if (task.raw) {
|
|
390
|
+
// Slash commands (e.g. /clear, /cost) usually don't emit a Stop hook.
|
|
391
|
+
// Treat inactivity as successful completion and keep the PTY alive.
|
|
392
|
+
const cleaned = cleanPtyOutput(session._buffer || '').trim();
|
|
393
|
+
const tail = cleaned.length > 2000 ? cleaned.slice(-2000) : cleaned;
|
|
394
|
+
const result = {
|
|
395
|
+
text: tail,
|
|
396
|
+
sessionId: session.sessionId || null,
|
|
397
|
+
cost: 0,
|
|
398
|
+
numTurns: 0,
|
|
399
|
+
durationMs: 0,
|
|
400
|
+
contextWindow: 0,
|
|
401
|
+
totalTokens: 0,
|
|
402
|
+
isError: false,
|
|
403
|
+
raw: true,
|
|
404
|
+
};
|
|
405
|
+
this.logger.info(`PTY raw command finished (no Stop signal) in ${workDir}`);
|
|
406
|
+
if (this.taskLogger) {
|
|
407
|
+
this.taskLogger.logAnswer(task.project || 'unknown', task.branch || 'main', tail, 0);
|
|
408
|
+
}
|
|
409
|
+
this.emit('complete', workDir, task, result);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
382
412
|
this.logger.warn(`PTY task timed out in ${workDir}`);
|
|
383
413
|
this._destroyPty(workDir);
|
|
384
414
|
this.emit('timeout', workDir, task);
|
|
@@ -406,9 +436,10 @@ export class PtyRunner extends EventEmitter {
|
|
|
406
436
|
// Filter out pipe-mode-specific args
|
|
407
437
|
const args = claudeArgs.filter(a => a !== '-p' && a !== '--output-format' && a !== 'json');
|
|
408
438
|
|
|
409
|
-
// Ensure --permission-mode is set
|
|
410
|
-
|
|
411
|
-
|
|
439
|
+
// Ensure --permission-mode is set to prevent interactive permission prompts.
|
|
440
|
+
// Use bypassPermissions as default — "auto" is not available on all plans/providers.
|
|
441
|
+
if (!args.includes('--permission-mode') && !args.includes('--dangerously-skip-permissions')) {
|
|
442
|
+
args.push('--permission-mode', 'bypassPermissions');
|
|
412
443
|
}
|
|
413
444
|
|
|
414
445
|
// Reduce PTY output noise: disable animations, progress bar, tips
|
package/listener/work-queue.js
CHANGED
|
@@ -30,7 +30,7 @@ export class WorkQueue {
|
|
|
30
30
|
* Enqueue a task for a workDir. Returns the task object.
|
|
31
31
|
* If no active task, marks it as ready to run immediately.
|
|
32
32
|
*/
|
|
33
|
-
enqueue (workDir, project, branch, text, telegramMessageId) {
|
|
33
|
+
enqueue (workDir, project, branch, text, telegramMessageId, raw = false) {
|
|
34
34
|
if (!this.queues[workDir]) {
|
|
35
35
|
this.queues[workDir] = {
|
|
36
36
|
project,
|
|
@@ -57,6 +57,7 @@ export class WorkQueue {
|
|
|
57
57
|
project,
|
|
58
58
|
branch: branch || 'main',
|
|
59
59
|
telegramMessageId,
|
|
60
|
+
raw: !!raw,
|
|
60
61
|
addedAt: new Date().toISOString(),
|
|
61
62
|
};
|
|
62
63
|
|
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.84",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|