claude-notification-plugin 1.0.104 → 1.0.106

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,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-notification-plugin",
3
- "version": "1.0.104",
3
+ "version": "1.0.106",
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/bin/uninstall.js CHANGED
@@ -1,239 +1,377 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'fs';
4
- import os from 'os';
5
- import path from 'path';
6
- import { execSync } from 'child_process';
7
-
8
- const home = os.homedir();
9
- const claudeDir = path.join(home, '.claude');
10
- const configPath = path.join(claudeDir, 'notifier.config.json');
11
- const settingsPath = path.join(claudeDir, 'settings.json');
12
- const statePath = path.join(claudeDir, '.notifier_state.json');
13
- const pidFile = path.join(claudeDir, '.listener.pid');
14
-
15
- const HOOK_COMMAND = 'claude-notify';
16
- const PLUGIN_KEY = 'claude-notification-plugin@bazilio-plugins';
17
- const MARKETPLACE_KEY = 'bazilio-plugins';
18
-
19
- function isPluginHookCommand (command) {
20
- if (typeof command !== 'string') {
21
- return false;
22
- }
23
-
24
- const normalized = command.trim().toLowerCase();
25
- if (!normalized) {
26
- return false;
27
- }
28
-
29
- return normalized === HOOK_COMMAND || normalized.startsWith(`${HOOK_COMMAND} `);
30
- }
31
-
32
- // Stop listener daemon if running
33
- let listenerStopped = false;
34
- try {
35
- if (fs.existsSync(pidFile)) {
36
- const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
37
- if (!isNaN(pid)) {
38
- try {
39
- if (process.platform === 'win32') {
40
- execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore', windowsHide: true });
41
- } else {
42
- process.kill(pid, 'SIGTERM');
43
- let tries = 10;
44
- const isAlive = (p) => {
45
- try {
46
- process.kill(p, 0);
47
- return true;
48
- } catch {
49
- return false;
50
- }
51
- };
52
- while (tries-- > 0 && isAlive(pid)) {
53
- execSync('sleep 0.5', { stdio: 'ignore' });
54
- }
55
- if (isAlive(pid)) {
56
- process.kill(pid, 'SIGKILL');
57
- }
58
- }
59
- listenerStopped = true;
60
- } catch {
61
- // process may already be dead
62
- }
63
- fs.unlinkSync(pidFile);
64
- }
65
- }
66
- } catch {
67
- // ignore
68
- }
69
-
70
- // Remove hooks from settings.json
71
- let hooksRemoved = false;
72
- let hooksRemoveError = '';
73
- if (fs.existsSync(settingsPath)) {
74
- try {
75
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
76
- let hadPluginHooks = false;
77
-
78
- if (settings.hooks) {
79
- for (const event of Object.keys(settings.hooks)) {
80
- const eventHooks = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
81
- const hadInEvent = eventHooks.some((matcher) =>
82
- matcher.hooks?.some((h) => isPluginHookCommand(h.command)),
83
- );
84
- hadPluginHooks = hadPluginHooks || hadInEvent;
85
-
86
- settings.hooks[event] = settings.hooks[event].filter((matcher) =>
87
- !matcher.hooks?.some((h) => isPluginHookCommand(h.command)),
88
- );
89
-
90
- if (settings.hooks[event].length === 0) {
91
- delete settings.hooks[event];
92
- }
93
- }
94
-
95
- if (Object.keys(settings.hooks).length === 0) {
96
- delete settings.hooks;
97
- }
98
- }
99
-
100
- // Remove plugin from enabledPlugins
101
- if (settings.enabledPlugins?.[PLUGIN_KEY]) {
102
- delete settings.enabledPlugins[PLUGIN_KEY];
103
- if (Object.keys(settings.enabledPlugins).length === 0) {
104
- delete settings.enabledPlugins;
105
- }
106
- }
107
-
108
- // Remove marketplace from extraKnownMarketplaces
109
- if (settings.extraKnownMarketplaces?.[MARKETPLACE_KEY]) {
110
- delete settings.extraKnownMarketplaces[MARKETPLACE_KEY];
111
- if (Object.keys(settings.extraKnownMarketplaces).length === 0) {
112
- delete settings.extraKnownMarketplaces;
113
- }
114
- }
115
-
116
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
117
-
118
- // Verify hooks were actually removed
119
- const verify = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
120
- const remainingPluginHooks = verify.hooks
121
- ? Object.values(verify.hooks).some((matchers) =>
122
- Array.isArray(matchers) &&
123
- matchers.some((m) => m.hooks?.some((h) => isPluginHookCommand(h.command))),
124
- )
125
- : false;
126
- hooksRemoved = hadPluginHooks && !remainingPluginHooks;
127
- if (hadPluginHooks && remainingPluginHooks) {
128
- hooksRemoveError = 'Hooks still present in settings.json after removal attempt';
129
- }
130
- } catch (err) {
131
- hooksRemoveError = `Failed to update settings.json: ${err.message}`;
132
- }
133
- }
134
-
135
- // Remove config, state, resolver, and listener files
136
- const resolverPath = path.join(claudeDir, 'claude-notify-resolve.js');
137
- const listenerLogFile = path.join(claudeDir, '.cc-n-listener.log');
138
- for (const file of [configPath, statePath, resolverPath, pidFile, listenerLogFile]) {
139
- if (fs.existsSync(file)) {
140
- fs.unlinkSync(file);
141
- }
142
- }
143
-
144
- // Remove CLI wrapper script
145
- const WRAPPER_NAMES = ['claude-notify'];
146
- let cliBinsRemoved = false;
147
- const ext = process.platform === 'win32' ? '.cmd' : '';
148
-
149
- // Collect directories to check for wrapper scripts
150
- const wrapperDirs = new Set();
151
-
152
- // Directory next to claude binary
153
- try {
154
- const cmd = process.platform === 'win32' ? 'where claude' : 'which claude';
155
- const claudeBin = execSync(cmd, { encoding: 'utf-8', windowsHide: true }).trim().split('\n')[0].trim();
156
- wrapperDirs.add(path.dirname(claudeBin));
157
- } catch {
158
- // claude not in PATH
159
- }
160
-
161
- // ~/.local/bin (common on Linux/macOS, also used on Windows)
162
- wrapperDirs.add(path.join(home, '.local', 'bin'));
163
-
164
- for (const dir of wrapperDirs) {
165
- for (const name of WRAPPER_NAMES) {
166
- const filePath = path.join(dir, `${name}${ext}`);
167
- if (fs.existsSync(filePath)) {
168
- fs.unlinkSync(filePath);
169
- cliBinsRemoved = true;
170
- }
171
- }
172
- }
173
-
174
- // Remove from installed_plugins.json
175
- const installedPluginsPath = path.join(claudeDir, 'plugins', 'installed_plugins.json');
176
- let pluginEntryRemoved = false;
177
- if (fs.existsSync(installedPluginsPath)) {
178
- try {
179
- const data = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
180
- if (data.plugins?.[PLUGIN_KEY]) {
181
- delete data.plugins[PLUGIN_KEY];
182
- fs.writeFileSync(installedPluginsPath, JSON.stringify(data, null, 2));
183
- pluginEntryRemoved = true;
184
- }
185
- } catch {
186
- // ignore
187
- }
188
- }
189
-
190
- // Remove plugin cache
191
- const pluginCacheDir = path.join(claudeDir, 'plugins', 'cache', 'bazilio-plugins', 'claude-notification-plugin');
192
- let cacheRemoved = false;
193
- if (fs.existsSync(pluginCacheDir)) {
194
- fs.rmSync(pluginCacheDir, { recursive: true, force: true });
195
- cacheRemoved = !fs.existsSync(pluginCacheDir);
196
- }
197
-
198
- const hooksStatus = hooksRemoved
199
- ? 'Hooks removed from settings.json'
200
- : hooksRemoveError
201
- ? `Warning: ${hooksRemoveError}`
202
- : 'No hooks found in settings.json';
203
-
204
- const extras = [
205
- listenerStopped ? 'Listener daemon stopped.' : '',
206
- pluginEntryRemoved || cacheRemoved ? 'Plugin registration cleaned.' : '',
207
- cliBinsRemoved ? 'CLI wrapper scripts removed.' : '',
208
- ].filter(Boolean).join('\n');
209
-
210
- const cacheStillExists = fs.existsSync(pluginCacheDir);
211
- if (cacheStillExists) {
212
- console.log(`
213
- Warning: Could not fully remove plugin cache directory:
214
- ${pluginCacheDir}
215
- Please remove it manually.
216
- ${hooksStatus}
217
- Config files deleted.${extras ? `\n${extras}` : ''}
218
- `);
219
- } else {
220
- console.log(`
221
- Claude Notification Plugin uninstalled.
222
- ${hooksStatus}
223
- Config files deleted.${extras ? `\n${extras}` : ''}
224
- `);
225
- }
226
-
227
- // If run manually (not via npm lifecycle), remove the global npm package too
228
- if (!process.env.npm_lifecycle_event) {
229
- try {
230
- console.log('Removing npm global package...');
231
- execSync('npm uninstall -g claude-notification-plugin', {
232
- stdio: 'inherit',
233
- windowsHide: true,
234
- });
235
- console.log('');
236
- } catch {
237
- console.log('Could not remove npm package automatically.\nRun manually: npm uninstall -g claude-notification-plugin\n');
238
- }
239
- }
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import { execSync } from 'child_process';
7
+
8
+ const home = os.homedir();
9
+ const claudeDir = path.join(home, '.claude');
10
+ const configPath = path.join(claudeDir, 'notifier.config.json');
11
+ const settingsPath = path.join(claudeDir, 'settings.json');
12
+ const statePath = path.join(claudeDir, '.notifier_state.json');
13
+ const pidFile = path.join(claudeDir, '.listener.pid');
14
+
15
+ const HOOK_COMMAND = 'claude-notify';
16
+ const PLUGIN_KEY = 'claude-notification-plugin@bazilio-plugins';
17
+ const MARKETPLACE_KEY = 'bazilio-plugins';
18
+
19
+ function isPluginHookCommand (command) {
20
+ if (typeof command !== 'string') {
21
+ return false;
22
+ }
23
+
24
+ const normalized = command.trim().toLowerCase();
25
+ if (!normalized) {
26
+ return false;
27
+ }
28
+
29
+ return normalized === HOOK_COMMAND || normalized.startsWith(`${HOOK_COMMAND} `);
30
+ }
31
+
32
+ function isProcessAlive (pid) {
33
+ if (!Number.isInteger(pid) || pid <= 0) {
34
+ return false;
35
+ }
36
+
37
+ try {
38
+ if (process.platform === 'win32') {
39
+ const result = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
40
+ encoding: 'utf-8',
41
+ windowsHide: true,
42
+ stdio: ['pipe', 'pipe', 'ignore'],
43
+ });
44
+ return result.includes(String(pid));
45
+ }
46
+
47
+ process.kill(pid, 0);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ function killProcessTree (pid) {
55
+ if (!isProcessAlive(pid)) {
56
+ return false;
57
+ }
58
+
59
+ try {
60
+ if (process.platform === 'win32') {
61
+ execSync(`taskkill /PID ${pid} /T /F`, {
62
+ stdio: 'ignore',
63
+ windowsHide: true,
64
+ });
65
+ console.log(`\x1b[33mListener process ${pid} killed\x1b[0m`);
66
+ return true;
67
+ }
68
+
69
+ process.kill(pid, 'SIGTERM');
70
+ let tries = 10;
71
+ while (tries-- > 0 && isProcessAlive(pid)) {
72
+ execSync('sleep 0.2', { stdio: 'ignore' });
73
+ }
74
+ if (isProcessAlive(pid)) {
75
+ process.kill(pid, 'SIGKILL');
76
+ }
77
+ console.log(`\x1b[33mListener process ${pid} killed\x1b[0m`);
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ function readPid (filePath) {
85
+ try {
86
+ if (!fs.existsSync(filePath)) {
87
+ return null;
88
+ }
89
+ const pid = parseInt(fs.readFileSync(filePath, 'utf-8').trim(), 10);
90
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function removeFileIfExists (filePath) {
97
+ try {
98
+ if (fs.existsSync(filePath)) {
99
+ fs.unlinkSync(filePath);
100
+ }
101
+ } catch {
102
+ // ignore
103
+ }
104
+ }
105
+
106
+ function findListenerPids () {
107
+ try {
108
+ if (process.platform === 'win32') {
109
+ const raw = execSync(
110
+ 'powershell -NoProfile -Command "Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress"',
111
+ {
112
+ encoding: 'utf-8',
113
+ windowsHide: true,
114
+ stdio: ['ignore', 'pipe', 'ignore'],
115
+ maxBuffer: 10 * 1024 * 1024,
116
+ },
117
+ ).trim();
118
+
119
+ if (!raw) {
120
+ return [];
121
+ }
122
+
123
+ const rows = JSON.parse(raw);
124
+ const processes = Array.isArray(rows) ? rows : [rows];
125
+ return processes
126
+ .filter((row) => /claude-notification-plugin[\\/]+listener[\\/]+listener\.js/i.test(row?.CommandLine || ''))
127
+ .map((row) => Number(row?.ProcessId))
128
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
129
+ }
130
+
131
+ const raw = execSync('ps -eo pid=,args=', {
132
+ encoding: 'utf-8',
133
+ stdio: ['ignore', 'pipe', 'ignore'],
134
+ });
135
+ const pids = [];
136
+ for (const line of raw.split('\n')) {
137
+ const match = line.trim().match(/^(\d+)\s+([\s\S]+)$/);
138
+ if (!match) {
139
+ continue;
140
+ }
141
+
142
+ const pid = parseInt(match[1], 10);
143
+ const args = match[2];
144
+ if (/claude-notification-plugin[\\/]+listener[\\/]+listener\.js/i.test(args)) {
145
+ pids.push(pid);
146
+ }
147
+ }
148
+ return pids;
149
+ } catch {
150
+ return [];
151
+ }
152
+ }
153
+
154
+ function stopListenerIfRunning () {
155
+ let stopped = false;
156
+ const processed = new Set();
157
+
158
+ const pidFromFile = readPid(pidFile);
159
+ if (pidFromFile) {
160
+ if (killProcessTree(pidFromFile)) {
161
+ stopped = true;
162
+ }
163
+ processed.add(pidFromFile);
164
+ }
165
+
166
+ for (const pid of findListenerPids()) {
167
+ if (processed.has(pid)) {
168
+ continue;
169
+ }
170
+ if (killProcessTree(pid)) {
171
+ stopped = true;
172
+ }
173
+ }
174
+
175
+ removeFileIfExists(pidFile);
176
+ return stopped;
177
+ }
178
+
179
+ // Stop listener daemon if running
180
+ const listenerStopped = stopListenerIfRunning();
181
+
182
+ // Remove hooks from settings.json
183
+ let hooksRemoved = false;
184
+ let hooksRemoveError = '';
185
+ if (fs.existsSync(settingsPath)) {
186
+ try {
187
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
188
+ let hadPluginHooks = false;
189
+
190
+ if (settings.hooks) {
191
+ for (const event of Object.keys(settings.hooks)) {
192
+ const eventHooks = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
193
+ const hadInEvent = eventHooks.some((matcher) =>
194
+ matcher.hooks?.some((h) => isPluginHookCommand(h.command)),
195
+ );
196
+ hadPluginHooks = hadPluginHooks || hadInEvent;
197
+
198
+ settings.hooks[event] = settings.hooks[event].filter((matcher) =>
199
+ !matcher.hooks?.some((h) => isPluginHookCommand(h.command)),
200
+ );
201
+
202
+ if (settings.hooks[event].length === 0) {
203
+ delete settings.hooks[event];
204
+ }
205
+ }
206
+
207
+ if (Object.keys(settings.hooks).length === 0) {
208
+ delete settings.hooks;
209
+ }
210
+ }
211
+
212
+ // Remove plugin from enabledPlugins
213
+ if (settings.enabledPlugins?.[PLUGIN_KEY]) {
214
+ delete settings.enabledPlugins[PLUGIN_KEY];
215
+ if (Object.keys(settings.enabledPlugins).length === 0) {
216
+ delete settings.enabledPlugins;
217
+ }
218
+ }
219
+
220
+ // Remove marketplace from extraKnownMarketplaces
221
+ if (settings.extraKnownMarketplaces?.[MARKETPLACE_KEY]) {
222
+ delete settings.extraKnownMarketplaces[MARKETPLACE_KEY];
223
+ if (Object.keys(settings.extraKnownMarketplaces).length === 0) {
224
+ delete settings.extraKnownMarketplaces;
225
+ }
226
+ }
227
+
228
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
229
+
230
+ // Verify hooks were actually removed
231
+ const verify = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
232
+ const remainingPluginHooks = verify.hooks
233
+ ? Object.values(verify.hooks).some((matchers) =>
234
+ Array.isArray(matchers) &&
235
+ matchers.some((m) => m.hooks?.some((h) => isPluginHookCommand(h.command))),
236
+ )
237
+ : false;
238
+ hooksRemoved = hadPluginHooks && !remainingPluginHooks;
239
+ if (hadPluginHooks && remainingPluginHooks) {
240
+ hooksRemoveError = 'Hooks still present in settings.json after removal attempt';
241
+ }
242
+ } catch (err) {
243
+ hooksRemoveError = `Failed to update settings.json: ${err.message}`;
244
+ }
245
+ }
246
+
247
+ // Remove config, state, resolver, and listener files
248
+ const resolverPath = path.join(claudeDir, 'claude-notify-resolve.js');
249
+ const listenerLogFile = path.join(claudeDir, '.cc-n-listener.log');
250
+ for (const file of [configPath, statePath, resolverPath, pidFile, listenerLogFile]) {
251
+ if (fs.existsSync(file)) {
252
+ fs.unlinkSync(file);
253
+ }
254
+ }
255
+
256
+ // Remove CLI wrapper script
257
+ const WRAPPER_NAMES = ['claude-notify'];
258
+ let cliBinsRemoved = false;
259
+ const ext = process.platform === 'win32' ? '.cmd' : '';
260
+
261
+ // Collect directories to check for wrapper scripts
262
+ const wrapperDirs = new Set();
263
+
264
+ // Directory next to claude binary
265
+ try {
266
+ const cmd = process.platform === 'win32' ? 'where claude' : 'which claude';
267
+ const claudeBin = execSync(cmd, { encoding: 'utf-8', windowsHide: true }).trim().split('\n')[0].trim();
268
+ wrapperDirs.add(path.dirname(claudeBin));
269
+ } catch {
270
+ // claude not in PATH
271
+ }
272
+
273
+ // ~/.local/bin (common on Linux/macOS, also used on Windows)
274
+ wrapperDirs.add(path.join(home, '.local', 'bin'));
275
+
276
+ for (const dir of wrapperDirs) {
277
+ for (const name of WRAPPER_NAMES) {
278
+ const filePath = path.join(dir, `${name}${ext}`);
279
+ if (fs.existsSync(filePath)) {
280
+ fs.unlinkSync(filePath);
281
+ cliBinsRemoved = true;
282
+ }
283
+ }
284
+ }
285
+
286
+ // Remove from installed_plugins.json
287
+ const installedPluginsPath = path.join(claudeDir, 'plugins', 'installed_plugins.json');
288
+ let pluginEntryRemoved = false;
289
+ if (fs.existsSync(installedPluginsPath)) {
290
+ try {
291
+ const data = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
292
+ if (data.plugins?.[PLUGIN_KEY]) {
293
+ delete data.plugins[PLUGIN_KEY];
294
+ fs.writeFileSync(installedPluginsPath, JSON.stringify(data, null, 2));
295
+ pluginEntryRemoved = true;
296
+ }
297
+ } catch {
298
+ // ignore
299
+ }
300
+ }
301
+
302
+ // Remove marketplace from known_marketplaces.json if no plugins reference it
303
+ const knownMarketplacesPath = path.join(claudeDir, 'plugins', 'known_marketplaces.json');
304
+ try {
305
+ if (fs.existsSync(knownMarketplacesPath)) {
306
+ // Check if any remaining plugins reference this marketplace
307
+ let hasMarketplacePlugins = false;
308
+ if (fs.existsSync(installedPluginsPath)) {
309
+ const data = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
310
+ if (data.plugins) {
311
+ hasMarketplacePlugins = Object.keys(data.plugins)
312
+ .some((key) => key.endsWith(`@${MARKETPLACE_KEY}`));
313
+ }
314
+ }
315
+
316
+ if (!hasMarketplacePlugins) {
317
+ const marketplaces = JSON.parse(fs.readFileSync(knownMarketplacesPath, 'utf-8'));
318
+ if (marketplaces[MARKETPLACE_KEY]) {
319
+ delete marketplaces[MARKETPLACE_KEY];
320
+ fs.writeFileSync(knownMarketplacesPath, JSON.stringify(marketplaces, null, 2));
321
+ }
322
+ }
323
+ }
324
+ } catch {
325
+ // ignore
326
+ }
327
+
328
+ // Remove plugin cache
329
+ const pluginCacheDir = path.join(claudeDir, 'plugins', 'cache', 'bazilio-plugins', 'claude-notification-plugin');
330
+ let cacheRemoved = false;
331
+ if (fs.existsSync(pluginCacheDir)) {
332
+ fs.rmSync(pluginCacheDir, { recursive: true, force: true });
333
+ cacheRemoved = !fs.existsSync(pluginCacheDir);
334
+ }
335
+
336
+ const hooksStatus = hooksRemoved
337
+ ? 'Hooks removed from settings.json'
338
+ : hooksRemoveError
339
+ ? `Warning: ${hooksRemoveError}`
340
+ : 'No hooks found in settings.json';
341
+
342
+ const extras = [
343
+ listenerStopped ? 'Listener daemon stopped.' : '',
344
+ pluginEntryRemoved || cacheRemoved ? 'Plugin registration cleaned.' : '',
345
+ cliBinsRemoved ? 'CLI wrapper scripts removed.' : '',
346
+ ].filter(Boolean).join('\n');
347
+
348
+ const cacheStillExists = fs.existsSync(pluginCacheDir);
349
+ if (cacheStillExists) {
350
+ console.log(`
351
+ Warning: Could not fully remove plugin cache directory:
352
+ ${pluginCacheDir}
353
+ Please remove it manually.
354
+ ${hooksStatus}
355
+ Config files deleted.${extras ? `\n${extras}` : ''}
356
+ `);
357
+ } else {
358
+ console.log(`
359
+ Claude Notification Plugin uninstalled.
360
+ ${hooksStatus}
361
+ Config files deleted.${extras ? `\n${extras}` : ''}
362
+ `);
363
+ }
364
+
365
+ // If run manually (not via npm lifecycle), remove the global npm package too
366
+ if (!process.env.npm_lifecycle_event) {
367
+ try {
368
+ console.log('Removing npm global package...');
369
+ execSync('npm uninstall -g claude-notification-plugin', {
370
+ stdio: 'inherit',
371
+ windowsHide: true,
372
+ });
373
+ console.log('');
374
+ } catch {
375
+ console.log('Could not remove npm package automatically.\nRun manually: npm uninstall -g claude-notification-plugin\n');
376
+ }
377
+ }
package/commit-sha CHANGED
@@ -1 +1 @@
1
- b8edcb1d6f6d799f0d00ee8c4ff4677b977c1b75
1
+ 3299ff296778c37204e4b4e5832343865faf035a
@@ -604,7 +604,7 @@ Bot: 📋 Queues:
604
604
  2. refactor the code
605
605
  ```
606
606
 
607
- ### /cancel — cancel a task
607
+ ### /cancel — stop a running task
608
608
 
609
609
  ```
610
610
  You: /cancel @api
@@ -195,7 +195,20 @@ function formatLabel (entry) {
195
195
  async function startTask (workDir, task) {
196
196
  const entry = queue.queues[workDir];
197
197
  const label = formatLabel(entry);
198
- const runningMsgId = await poller.sendMessage(`⏳ [${label}] Running: ${escapeHtml(task.text)}`, task.telegramMessageId);
198
+ const runningShort = `⏳ [${label}] Running...`;
199
+ const runningFull = `⏳ [${label}] Running: ${escapeHtml(task.text)}`;
200
+ let runningMsgId = null;
201
+
202
+ if (task.telegramMessageId) {
203
+ // In replies, the quoted user message already contains task text.
204
+ runningMsgId = await poller.sendMessage(runningShort, task.telegramMessageId);
205
+ if (!runningMsgId) {
206
+ runningMsgId = await poller.sendMessage(runningFull);
207
+ }
208
+ } else {
209
+ runningMsgId = await poller.sendMessage(runningFull);
210
+ }
211
+
199
212
  task.runningMessageId = runningMsgId;
200
213
  try {
201
214
  const started = runner.run(workDir, task);
@@ -511,24 +524,26 @@ async function handleStop () {
511
524
  }
512
525
 
513
526
  function handleHelp () {
514
- return '<b>📖 Commands:</b>\n'
515
- + '\n/status — status of all projects'
516
- + '\n/status @project project status'
517
- + '\n/queueall queues'
518
- + '\n/cancel [@project[/branch]] cancel task'
519
- + '\n/drop @project N remove task from queue'
520
- + '\n/clear @project[/branch]clear queue'
521
- + '\n/projectslist projects'
522
- + '\n/worktrees @project project worktrees'
523
- + '\n/worktree @project branch create worktree'
524
- + '\n/rmworktree @project branch — remove worktree'
525
- + '\n/historytask history'
526
- + '\n/stopstop listener'
527
- + '\n/helpthis help'
528
- + '\n\n<b>Tasks:</b>'
529
- + '\n<code>@project task</code> — main worktree'
530
- + '\n<code>@project/branch task</code> — worktree'
531
- + '\n<code>task</code> — default project';
527
+ return `<b>📖 Commands:</b>
528
+
529
+ /status — status of all projects
530
+ /status @project project status
531
+ /queueall queues
532
+ /cancel [@project[/branch]]cancel task
533
+ /drop @project N remove task from queue
534
+ /clear @project[/branch]clear queue
535
+ /projectslist projects
536
+ /worktrees @project — project worktrees
537
+ /worktree @project branch — create worktree
538
+ /rmworktree @project branch remove worktree
539
+ /historytask history
540
+ /stopstop listener
541
+ /help — this help
542
+
543
+ <b>Tasks:</b>
544
+ <code>@project task</code> — main worktree
545
+ <code>@project/branch task</code> — worktree
546
+ <code>task</code> — default project`;
532
547
  }
533
548
 
534
549
  // ----------------------
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.0.104",
4
+ "version": "1.0.106",
5
5
  "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
6
  "type": "module",
7
7
  "engines": {