claude-notification-plugin 1.1.105 → 1.1.108

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.
@@ -1,20 +1,20 @@
1
- {
2
- "name": "claude-notification-plugin",
3
- "version": "1.1.105",
4
- "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
5
- "author": {
6
- "name": "Viacheslav Makarov",
7
- "email": "npmjs@bazilio.ru"
8
- },
9
- "homepage": "https://github.com/Bazilio-san/claude-notification-plugin#readme",
10
- "repository": "https://github.com/Bazilio-san/claude-notification-plugin",
11
- "license": "MIT",
12
- "keywords": [
13
- "notification",
14
- "telegram",
15
- "windows",
16
- "sound",
17
- "voice",
18
- "hooks"
19
- ]
20
- }
1
+ {
2
+ "name": "claude-notification-plugin",
3
+ "version": "1.1.108",
4
+ "description": "Telegram listener daemon + Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
5
+ "author": {
6
+ "name": "Viacheslav Makarov",
7
+ "email": "npmjs@bazilio.ru"
8
+ },
9
+ "homepage": "https://github.com/Bazilio-san/claude-notification-plugin#readme",
10
+ "repository": "https://github.com/Bazilio-san/claude-notification-plugin",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "notification",
14
+ "telegram",
15
+ "windows",
16
+ "sound",
17
+ "voice",
18
+ "hooks"
19
+ ]
20
+ }
package/README.md CHANGED
@@ -2,11 +2,23 @@
2
2
 
3
3
  **Send a message in Telegram, and the task starts running on your PC.**
4
4
 
5
- Cross-platform notifications for Claude Code task completion.
6
- Sends alerts to Telegram and desktop (Windows, macOS, Linux) when Claude finishes working.
7
-
8
-
9
- ## Features
5
+ Cross-platform notifications for Claude Code task completion.
6
+ Sends alerts to Telegram and desktop (Windows, macOS, Linux) when Claude finishes working.
7
+
8
+ ## Start Here (Listener)
9
+
10
+ If you want Telegram-first remote control, start with the Listener daemon:
11
+
12
+ ```bash
13
+ claude-notify listener setup
14
+ claude-notify listener start
15
+ claude-notify listener status
16
+ ```
17
+
18
+ Deep internals and troubleshooting: [Detailed Guide](listener/LISTENER-DETAILED.md)
19
+
20
+
21
+ ## Features
10
22
 
11
23
  - **[Telegram Listener](#telegram-listener)** — your remote control for Claude (supports worktrees)
12
24
  - Telegram bot messages with auto-delete
@@ -118,7 +130,7 @@ ENV: `CLAUDE_NOTIFY_TELEGRAM_TOKEN`
118
130
  **telegram.chatId** — Chat ID to send messages to.
119
131
  ENV: `CLAUDE_NOTIFY_TELEGRAM_CHAT_ID`
120
132
 
121
- **telegram.deleteAfterHours** — Auto-delete old Telegram messages after N hours. `0` to disable. Default: **24**
133
+ **telegram.deleteAfterHours** — Auto-delete old Telegram messages after N hours (applies to notifier and listener bot messages). `0` to disable. Default: **24**
122
134
 
123
135
  **telegram.includeLastCcMessageInTelegram** — Append Claude's last message to the notification (truncated to 3500 chars). Default: **true**
124
136
  ENV: `CLAUDE_NOTIFY_INCLUDE_LAST_CC_MESSAGE_IN_TELEGRAM`
@@ -219,14 +231,16 @@ fix the login bug → runs in "default" project
219
231
  &api/feature/auth implement OAuth2 → runs in a worktree (auto-created)
220
232
  ```
221
233
 
222
- The bot replies with status and results:
234
+ The bot replies with status and results:
223
235
 
224
236
  ```
225
237
  ⏳ [&api] Running: add pagination to GET /users
226
238
  ...
227
- ✅ [&api] Done: add pagination to GET /users
228
- <claude's output>
229
- ```
239
+ ✅ [&api] Done: add pagination to GET /users
240
+ <claude's output>
241
+ ```
242
+
243
+ If Claude returns a temporary API failure (`StopFailure`, e.g. `529 overloaded`), the listener saves the reported session ID and the next task for the same target auto-resumes it.
230
244
 
231
245
  ### 4. Manage the daemon
232
246
 
package/bin/cli.js CHANGED
@@ -32,10 +32,11 @@ switch (command) {
32
32
  console.log(`Usage: claude-notify <command> [options]
33
33
 
34
34
  Commands:
35
+ listener <action> Manage the Telegram Listener daemon
36
+ Actions: start, stop, status, logs, restart
35
37
  install Setup plugin registration, Telegram config, hooks
36
38
  uninstall Remove plugin, hooks, config, CLI wrappers
37
- listener <action> Manage the Telegram Listener daemon
38
- Actions: start, stop, status, logs, restart`);
39
+ `);
39
40
  process.exit(command ? 1 : 0);
40
41
  }
41
42
  }
package/bin/install.js CHANGED
@@ -737,11 +737,16 @@ Plugin hooks (via hooks/hooks.json):
737
737
  Config: ${CONFIG_PATH}
738
738
  ${telegramStatus}${platformTip}
739
739
 
740
- Log: ${INSTALL_LOG_PATH}
741
-
742
- To uninstall: claude-notify uninstall
743
-
744
- To disable per project, add to .claude/settings.local.json: { "env": { "CLAUDE_NOTIFY_DISABLE": "1" } }`);
740
+ Log: ${INSTALL_LOG_PATH}
741
+
742
+ Listener quick start:
743
+ claude-notify listener setup
744
+ claude-notify listener start
745
+ claude-notify listener status
746
+
747
+ To uninstall: claude-notify uninstall
748
+
749
+ To disable per project, add to .claude/settings.local.json: { "env": { "CLAUDE_NOTIFY_DISABLE": "1" } }`);
745
750
 
746
751
  closeLog();
747
752
  }
package/commit-sha CHANGED
@@ -1 +1 @@
1
- 172c80dbeb3e3f935e6e96b28d23b7ee40ba8858
1
+ 0daaa9a945591246c6de6c117d9cf9b4add7ab25
@@ -3,7 +3,7 @@
3
3
  Telegram Listener is a background daemon that receives tasks from a Telegram chat
4
4
  and executes them on your machine via an interactive Claude Code PTY session. The result is sent back to Telegram.
5
5
 
6
- **[Quick Start here](../LISTENER.md)**
6
+ **[Quick Start here](../README.md#telegram-listener)**
7
7
 
8
8
  # Detailed Guide
9
9
 
@@ -133,7 +133,9 @@ const resumeLastSessionEnabled = listenerConfig.resumeLastSession !== false; //
133
133
  const sessionsListLimit = listenerConfig.sessionsListLimit || 5;
134
134
  const sessionWorkingThresholdSec = listenerConfig.sessionWorkingThresholdSec || 2;
135
135
 
136
- const poller = new TelegramPoller(token, chatId, logger);
136
+ const poller = new TelegramPoller(token, chatId, logger, {
137
+ deleteAfterHours: config.telegram?.deleteAfterHours,
138
+ });
137
139
  const queue = new WorkQueue(
138
140
  logger,
139
141
  listenerConfig.maxQueuePerWorkDir || 10,
@@ -304,7 +306,14 @@ async function notifyTaskCompletion (workDir, task, kind, payload = {}) {
304
306
  let header;
305
307
  let queueResult;
306
308
  if (kind === 'error') {
307
- header = `❌ <code>${label}</code>\nError`;
309
+ const resumeSid = payload.resumeSessionId || payload.sessionId || null;
310
+ if (resumeSid) {
311
+ setStoredSessionId(workDir, resumeSid);
312
+ }
313
+ const resumeHint = resumeSid
314
+ ? `\nSaved session: <code>${resumeSid}</code>\nNext task for this target will auto-resume it.`
315
+ : '';
316
+ header = `❌ <code>${label}</code>\nError${resumeHint}`;
308
317
  queueResult = `ERROR: ${payload.errorMsg}`;
309
318
  } else if (kind === 'timeout') {
310
319
  const reason = payload.reason || `no activity for ${payload.timeoutMin} min`;
@@ -413,7 +422,17 @@ async function notifyTaskCompletion (workDir, task, kind, payload = {}) {
413
422
  }
414
423
 
415
424
  runner.on('complete', (workDir, task, result) => notifyTaskCompletion(workDir, task, 'complete', result));
416
- runner.on('error', (workDir, task, errorMsg) => notifyTaskCompletion(workDir, task, 'error', { errorMsg }));
425
+ runner.on('error', (workDir, task, errorData) => {
426
+ if (typeof errorData === 'string') {
427
+ notifyTaskCompletion(workDir, task, 'error', { errorMsg: errorData });
428
+ return;
429
+ }
430
+ notifyTaskCompletion(workDir, task, 'error', {
431
+ errorMsg: errorData?.message || 'Unknown error',
432
+ sessionId: errorData?.sessionId || null,
433
+ resumeSessionId: errorData?.resumeSessionId || null,
434
+ });
435
+ });
417
436
  runner.on('timeout', (workDir, task) => notifyTaskCompletion(workDir, task, 'timeout', {
418
437
  timeoutMin: Math.round(taskTimeout / 60000),
419
438
  }));
@@ -154,6 +154,8 @@ export class PtyRunner extends EventEmitter {
154
154
  } else if (type === 'error') {
155
155
  // StopFailure — emit error, abort task
156
156
  this._unlinkSafe(filePath);
157
+ const errorSignalSessionId = marker.sessionId
158
+ || (f.startsWith('err_') ? f.slice(4, -5) : null);
157
159
  for (const [workDir, session] of this.sessions) {
158
160
  if (session.state === 'busy' && this._normalizePath(session.workDir) === this._normalizePath(cwd)) {
159
161
  if (session._pendingId && this.pendingMarkers.has(session._pendingId)) {
@@ -164,11 +166,22 @@ export class PtyRunner extends EventEmitter {
164
166
  session.currentTask = null;
165
167
  this._destroyPty(workDir);
166
168
  const errorMsg = `API error: ${marker.error}${marker.errorDetails ? ' — ' + marker.errorDetails : ''}`;
169
+ const resumeMatch = (marker.errorDetails || marker.lastAssistantMessage || '')
170
+ .match(/(?:^|\s)--resume\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
171
+ const resumeSessionId = resumeMatch?.[1] || null;
167
172
  this.logger.error(`Hook signal: ${errorMsg} in ${workDir}`);
168
173
  if (this.taskLogger) {
169
174
  this.taskLogger.logAnswer(task?.project || 'unknown', task?.branch || 'main', errorMsg, 1);
170
175
  }
171
- this.emit('error', workDir, task, errorMsg);
176
+ this.emit('error', workDir, task, {
177
+ message: errorMsg,
178
+ sessionId: errorSignalSessionId && errorSignalSessionId !== 'unknown'
179
+ ? errorSignalSessionId
180
+ : null,
181
+ resumeSessionId,
182
+ errorType: marker.error || 'unknown',
183
+ errorDetails: marker.errorDetails || '',
184
+ });
172
185
  break;
173
186
  }
174
187
  }
@@ -7,17 +7,22 @@ const MAX_MESSAGE_LENGTH = 4096;
7
7
  // of looping silently.
8
8
  const MAX_CONSECUTIVE_409 = 8;
9
9
 
10
- export class TelegramPoller {
11
- constructor (token, chatId, logger) {
12
- this.token = token;
13
- this.chatId = String(chatId);
14
- this.logger = logger;
15
- this.baseUrl = `https://api.telegram.org/bot${token}`;
16
- this.offset = 0;
17
- this._errorBackoff = 0; // current backoff in ms (0 = no backoff)
18
- this._consecutiveErrors = 0;
19
- this._consecutive409 = 0;
20
- }
10
+ export class TelegramPoller {
11
+ constructor (token, chatId, logger, options = {}) {
12
+ this.token = token;
13
+ this.chatId = String(chatId);
14
+ this.logger = logger;
15
+ this.baseUrl = `https://api.telegram.org/bot${token}`;
16
+ this.offset = 0;
17
+ this._errorBackoff = 0; // current backoff in ms (0 = no backoff)
18
+ this._consecutiveErrors = 0;
19
+ this._consecutive409 = 0;
20
+ const deleteAfterHours = Number(options.deleteAfterHours);
21
+ this._deleteAfterMs = Number.isFinite(deleteAfterHours) && deleteAfterHours > 0
22
+ ? deleteAfterHours * 3600_000
23
+ : 0;
24
+ this._sentMessages = [];
25
+ }
21
26
 
22
27
  async flush () {
23
28
  try {
@@ -153,9 +158,12 @@ export class TelegramPoller {
153
158
  body: JSON.stringify({ ...base, parse_mode: 'HTML' }),
154
159
  });
155
160
  let data = await res.json();
156
- if (data.ok) {
157
- return data.result.message_id;
158
- }
161
+ if (data.ok) {
162
+ const messageId = data.result.message_id;
163
+ this._trackSentMessage(messageId);
164
+ await this._cleanupOldMessages();
165
+ return messageId;
166
+ }
159
167
  const htmlErr = data.description || `error_code ${data.error_code}`;
160
168
  // Retry without HTML parse mode (covers entity-parsing errors)
161
169
  res = await fetch(`${this.baseUrl}/sendMessage`, {
@@ -164,10 +172,13 @@ export class TelegramPoller {
164
172
  body: JSON.stringify(base),
165
173
  });
166
174
  data = await res.json();
167
- if (data.ok) {
168
- this.logger.warn(`sendMessage: HTML failed (${htmlErr}), plain succeeded`);
169
- return data.result.message_id;
170
- }
175
+ if (data.ok) {
176
+ this.logger.warn(`sendMessage: HTML failed (${htmlErr}), plain succeeded`);
177
+ const messageId = data.result.message_id;
178
+ this._trackSentMessage(messageId);
179
+ await this._cleanupOldMessages();
180
+ return messageId;
181
+ }
171
182
  this.logger.error(`sendMessage failed: HTML=${htmlErr}, plain=${data.description || data.error_code}`);
172
183
  return null;
173
184
  } catch (err) {
@@ -262,23 +273,57 @@ export class TelegramPoller {
262
273
  }
263
274
  }
264
275
 
265
- async sendDocument (buffer, filename, caption) {
266
- try {
267
- const formData = new FormData();
268
- formData.append('chat_id', this.chatId);
269
- formData.append('document', new Blob([buffer]), filename);
270
- if (caption) {
271
- formData.append('caption', caption.slice(0, 1024));
272
- }
273
- await fetch(`${this.baseUrl}/sendDocument`, {
274
- method: 'POST',
275
- body: formData,
276
- });
277
- } catch (err) {
278
- this.logger.error(`sendDocument error: ${err.message}`);
279
- }
280
- }
281
- }
276
+ async sendDocument (buffer, filename, caption) {
277
+ try {
278
+ const formData = new FormData();
279
+ formData.append('chat_id', this.chatId);
280
+ formData.append('document', new Blob([buffer]), filename);
281
+ if (caption) {
282
+ formData.append('caption', caption.slice(0, 1024));
283
+ }
284
+ const res = await fetch(`${this.baseUrl}/sendDocument`, {
285
+ method: 'POST',
286
+ body: formData,
287
+ });
288
+ const data = await res.json();
289
+ if (data.ok && data.result?.message_id) {
290
+ this._trackSentMessage(data.result.message_id);
291
+ await this._cleanupOldMessages();
292
+ }
293
+ } catch (err) {
294
+ this.logger.error(`sendDocument error: ${err.message}`);
295
+ }
296
+ }
297
+
298
+ _trackSentMessage (messageId) {
299
+ if (!messageId || this._deleteAfterMs <= 0) {
300
+ return;
301
+ }
302
+ this._sentMessages.push({
303
+ id: messageId,
304
+ ts: Date.now(),
305
+ });
306
+ if (this._sentMessages.length > 1000) {
307
+ this._sentMessages = this._sentMessages.slice(-500);
308
+ }
309
+ }
310
+
311
+ async _cleanupOldMessages () {
312
+ if (this._deleteAfterMs <= 0 || this._sentMessages.length === 0) {
313
+ return;
314
+ }
315
+ const now = Date.now();
316
+ const keep = [];
317
+ for (const msg of this._sentMessages) {
318
+ if (now - msg.ts > this._deleteAfterMs) {
319
+ await this.deleteMessage(msg.id);
320
+ } else {
321
+ keep.push(msg);
322
+ }
323
+ }
324
+ this._sentMessages = keep;
325
+ }
326
+ }
282
327
 
283
328
  function escapeHtml (text) {
284
329
  return text
@@ -214,15 +214,16 @@ function writePtySignalFile (event) {
214
214
  });
215
215
  }
216
216
 
217
- function writeErrorSignalFile (event) {
218
- const sessionId = event.session_id || 'unknown';
219
- writeSignalFile(`err_${sessionId}.json`, {
220
- type: 'error',
221
- cwd: event.cwd || process.cwd(),
222
- error: event.error || 'unknown',
223
- errorDetails: event.error_details || '',
224
- lastAssistantMessage: event.last_assistant_message || '',
225
- timestamp: Date.now(),
217
+ function writeErrorSignalFile (event) {
218
+ const sessionId = event.session_id || 'unknown';
219
+ writeSignalFile(`err_${sessionId}.json`, {
220
+ type: 'error',
221
+ sessionId,
222
+ cwd: event.cwd || process.cwd(),
223
+ error: event.error || 'unknown',
224
+ errorDetails: event.error_details || '',
225
+ lastAssistantMessage: event.last_assistant_message || '',
226
+ timestamp: Date.now(),
226
227
  });
227
228
  }
228
229
 
@@ -413,8 +414,11 @@ async function sendTelegram (config, state) {
413
414
  }
414
415
 
415
416
  // Delete old messages
416
- const maxAge = (config.telegram.deleteAfterHours || 24) * 3600_000;
417
- if (state.sentMessages?.length) {
417
+ const deleteAfter = Number(config.telegram.deleteAfterHours);
418
+ const maxAge = Number.isFinite(deleteAfter) && deleteAfter > 0
419
+ ? deleteAfter * 3600_000
420
+ : 0;
421
+ if (maxAge > 0 && state.sentMessages?.length) {
418
422
  const now = Date.now();
419
423
  const keep = [];
420
424
  for (const msg of state.sentMessages) {
package/package.json CHANGED
@@ -1,66 +1,69 @@
1
- {
2
- "name": "claude-notification-plugin",
3
- "productName": "claude-notification-plugin",
4
- "version": "1.1.105",
5
- "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
- "type": "module",
7
- "engines": {
8
- "node": ">=18.0.0"
9
- },
10
- "files": [
11
- ".claude-plugin/",
12
- "bin/",
13
- "claude_img/claude.png",
14
- "hooks/",
15
- "listener/",
16
- "notifier/",
17
- "commit-sha",
18
- "README.md",
19
- "LICENSE"
20
- ],
21
- "bin": {
22
- "claude-notify": "bin/cli.js"
23
- },
24
- "scripts": {
25
- "prepack": "git rev-parse HEAD > commit-sha",
26
- "postinstall": "node bin/install.js",
27
- "lint": "eslint .",
28
- "lint:fix": "eslint --fix .",
29
- "listener:restart": "claude-notify listener restart",
30
- "listener:stop": "claude-notify listener stop",
31
- "listener:start": "claude-notify listener start",
32
- "listener:status": "claude-notify listener status"
33
- },
34
- "keywords": [
35
- "claude",
36
- "claude-code",
37
- "notifications",
38
- "telegram",
39
- "hooks",
40
- "macos",
41
- "linux",
42
- "cross-platform"
43
- ],
44
- "author": {
45
- "name": "Viacheslav Makarov",
46
- "email": "npmjs@bazilio.ru"
47
- },
48
- "license": "MIT",
49
- "repository": {
50
- "type": "git",
51
- "url": "git+https://github.com/Bazilio-san/claude-notification-plugin.git"
52
- },
53
- "homepage": "https://github.com/Bazilio-san/claude-notification-plugin#readme",
54
- "publishConfig": {
55
- "access": "public"
56
- },
57
- "dependencies": {
58
- "@xterm/headless": "^6.0.0",
59
- "node-notifier": "^10.0.1",
60
- "node-pty": "^1.1.0"
61
- },
62
- "devDependencies": {
63
- "eslint-plugin-import": "^2.31.0",
64
- "eslint-plugin-unused-imports": "^4.4.1"
65
- }
66
- }
1
+ {
2
+ "name": "claude-notification-plugin",
3
+ "productName": "claude-notification-plugin",
4
+ "version": "1.1.108",
5
+ "description": "Telegram listener daemon + Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=18.0.0"
9
+ },
10
+ "files": [
11
+ ".claude-plugin/",
12
+ "bin/",
13
+ "claude_img/claude.png",
14
+ "hooks/",
15
+ "listener/",
16
+ "notifier/",
17
+ "commit-sha",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "bin": {
22
+ "claude-notify": "bin/cli.js"
23
+ },
24
+ "scripts": {
25
+ "prepack": "git rev-parse HEAD > commit-sha",
26
+ "postinstall": "node bin/install.js",
27
+ "lint": "eslint .",
28
+ "lint:fix": "eslint --fix .",
29
+ "listener:restart": "claude-notify listener restart",
30
+ "listener:stop": "claude-notify listener stop",
31
+ "listener:start": "claude-notify listener start",
32
+ "listener:status": "claude-notify listener status",
33
+ "agents:link": "node scripts/claude-2-agents-symlink.js setup",
34
+ "agents:link:status": "node scripts/claude-2-agents-symlink.js status",
35
+ "agents:link:remove": "node scripts/claude-2-agents-symlink.js remove"
36
+ },
37
+ "keywords": [
38
+ "claude",
39
+ "claude-code",
40
+ "notifications",
41
+ "telegram",
42
+ "hooks",
43
+ "macos",
44
+ "linux",
45
+ "cross-platform"
46
+ ],
47
+ "author": {
48
+ "name": "Viacheslav Makarov",
49
+ "email": "npmjs@bazilio.ru"
50
+ },
51
+ "license": "MIT",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/Bazilio-san/claude-notification-plugin.git"
55
+ },
56
+ "homepage": "https://github.com/Bazilio-san/claude-notification-plugin#readme",
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "dependencies": {
61
+ "@xterm/headless": "^6.0.0",
62
+ "node-notifier": "^10.0.1",
63
+ "node-pty": "^1.1.0"
64
+ },
65
+ "devDependencies": {
66
+ "eslint-plugin-import": "^2.31.0",
67
+ "eslint-plugin-unused-imports": "^4.4.1"
68
+ }
69
+ }