claude-notification-plugin 1.0.98 → 1.0.101
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 +41 -15
- package/bin/cli.js +7 -7
- package/bin/install.js +1 -3
- package/bin/listener-cli.js +90 -13
- package/bin/uninstall.js +66 -26
- package/commit-sha +1 -1
- package/listener/LISTENER-DETAILED.md +20 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.101",
|
|
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
|
@@ -24,6 +24,23 @@ Sends alerts to Telegram and desktop (Windows, macOS, Linux) when Claude finishe
|
|
|
24
24
|
npm install -g claude-notification-plugin
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
|
|
28
|
+
## Telegram Setup
|
|
29
|
+
|
|
30
|
+
If you plan to work with Telegram, you need to pre-register the bot and send a message to it
|
|
31
|
+
|
|
32
|
+
1. Open Telegram, find **@BotFather**
|
|
33
|
+
2. Send `/newbot`, follow prompts, pick a name
|
|
34
|
+
3. Copy the bot token (format: `123456789:ABCdef...`)
|
|
35
|
+
4. **Send any message to your new bot**
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
claude-notify install
|
|
42
|
+
```
|
|
43
|
+
|
|
27
44
|
The installer prompts for Telegram bot credentials and sets everything up.
|
|
28
45
|
Re-run `claude-notify install` anytime to reconfigure.
|
|
29
46
|
|
|
@@ -151,24 +168,21 @@ Add to `.claude/settings.local.json` in the project root:
|
|
|
151
168
|
}
|
|
152
169
|
```
|
|
153
170
|
|
|
154
|
-
## Telegram Setup
|
|
155
|
-
|
|
156
|
-
1. Open Telegram, find **@BotFather**
|
|
157
|
-
2. Send `/newbot`, follow prompts, pick a name
|
|
158
|
-
3. Copy the bot token (format: `123456789:ABCdef...`)
|
|
159
|
-
4. **Send any message to your new bot**
|
|
160
|
-
5. Open `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates`
|
|
161
|
-
6. Find `"chat":{"id":123456789}` in the response — that's your Chat ID
|
|
162
|
-
|
|
163
|
-
Alternative: add **@userinfobot** to a chat and it will reply with the ID.
|
|
164
|
-
|
|
165
171
|
## Telegram Listener
|
|
166
172
|
|
|
167
173
|
Background daemon that receives tasks from Telegram and executes them via `claude -p`. The result is sent back to Telegram.
|
|
168
174
|
|
|
169
175
|
The Listener uses the same bot and `chatId` as notifications.
|
|
170
176
|
|
|
171
|
-
### 1.
|
|
177
|
+
### 1. Configure the listener
|
|
178
|
+
|
|
179
|
+
Run the interactive setup wizard:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
claude-notify listener setup
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Alternatively, add a `listener` section to config manually:
|
|
172
186
|
|
|
173
187
|
```json
|
|
174
188
|
{
|
|
@@ -212,9 +226,10 @@ The bot replies with status and results:
|
|
|
212
226
|
|
|
213
227
|
```bash
|
|
214
228
|
claude-notify listener status # Check if running
|
|
215
|
-
claude-notify listener stop
|
|
216
|
-
claude-notify listener restart
|
|
229
|
+
claude-notify listener stop
|
|
230
|
+
claude-notify listener restart
|
|
217
231
|
claude-notify listener logs # View last 50 log lines
|
|
232
|
+
claude-notify listener setup # Interactive listener configuration
|
|
218
233
|
```
|
|
219
234
|
|
|
220
235
|
### 5. Bot commands
|
|
@@ -264,14 +279,25 @@ Worktrees are auto-created when you use `@project/branch` syntax (controlled by
|
|
|
264
279
|
/rmworktree @api feature/payments ← remove
|
|
265
280
|
```
|
|
266
281
|
|
|
282
|
+
|
|
267
283
|
[Detailed Guide](listener/LISTENER-DETAILED.md) — internals, architecture, troubleshooting, full session example.
|
|
268
284
|
|
|
285
|
+
|
|
286
|
+
## Manual Telegram bot chatId retrieval:
|
|
287
|
+
|
|
288
|
+
- Open `https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates`
|
|
289
|
+
- Find `"chat":{"id":123456789}` in the response — that's your Chat ID
|
|
290
|
+
|
|
291
|
+
Alternative: add **@userinfobot** to a chat and it will reply with the ID.
|
|
292
|
+
|
|
293
|
+
|
|
269
294
|
## CLI Commands
|
|
270
295
|
|
|
271
296
|
```
|
|
272
297
|
claude-notify install Reinstall plugin registration, Telegram config, hooks
|
|
298
|
+
claude-notify uninstall Remove plugin, hooks, config, CLI wrappers
|
|
273
299
|
claude-notify listener <action> Manage the Telegram Listener daemon
|
|
274
|
-
Actions: start, stop, status, logs, restart
|
|
300
|
+
Actions: start, stop, status, setup, logs, restart
|
|
275
301
|
```
|
|
276
302
|
|
|
277
303
|
## License
|
package/bin/cli.js
CHANGED
|
@@ -29,13 +29,13 @@ switch (command) {
|
|
|
29
29
|
if (!process.stdin.isTTY) {
|
|
30
30
|
await import(toImportUrl(path.join('..', 'notifier', 'notifier.js')));
|
|
31
31
|
} else {
|
|
32
|
-
console.log(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
console.log(`Usage: claude-notify <command> [options]
|
|
33
|
+
|
|
34
|
+
Commands:
|
|
35
|
+
install Setup plugin registration, Telegram config, hooks
|
|
36
|
+
uninstall Remove plugin, hooks, config, CLI wrappers
|
|
37
|
+
listener <action> Manage the Telegram Listener daemon
|
|
38
|
+
Actions: start, stop, status, logs, restart`);
|
|
39
39
|
process.exit(command ? 1 : 0);
|
|
40
40
|
}
|
|
41
41
|
}
|
package/bin/install.js
CHANGED
|
@@ -526,9 +526,7 @@ ${telegramStatus}${platformTip}
|
|
|
526
526
|
|
|
527
527
|
To uninstall: claude-notify uninstall
|
|
528
528
|
|
|
529
|
-
To disable per project, add to .claude/settings.local.json:
|
|
530
|
-
{ "env": { "CLAUDE_NOTIFY_DISABLE": "1" } }
|
|
531
|
-
`);
|
|
529
|
+
To disable per project, add to .claude/settings.local.json: { "env": { "CLAUDE_NOTIFY_DISABLE": "1" } }`);
|
|
532
530
|
}
|
|
533
531
|
|
|
534
532
|
main().then(() => 0);
|
package/bin/listener-cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import readline from 'readline';
|
|
6
7
|
import { spawn, execSync } from 'child_process';
|
|
7
8
|
import { fileURLToPath } from 'url';
|
|
8
9
|
|
|
@@ -43,15 +44,19 @@ switch (command) {
|
|
|
43
44
|
stopDaemon();
|
|
44
45
|
setTimeout(() => startDaemon(), 1000);
|
|
45
46
|
break;
|
|
47
|
+
case 'setup':
|
|
48
|
+
setupListener();
|
|
49
|
+
break;
|
|
46
50
|
default:
|
|
47
|
-
console.log(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
console.log(`Usage: claude-notify listener <start|stop|status|setup|logs|restart>
|
|
52
|
+
|
|
53
|
+
Commands:
|
|
54
|
+
start Start the listener daemon
|
|
55
|
+
stop Stop the listener daemon
|
|
56
|
+
status Show daemon status
|
|
57
|
+
setup Interactive listener configuration
|
|
58
|
+
logs Show recent log entries
|
|
59
|
+
restart Restart the daemon`);
|
|
55
60
|
process.exit(command ? 1 : 0);
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -132,9 +137,9 @@ function startDaemon () {
|
|
|
132
137
|
fs.mkdirSync(path.dirname(PID_FILE), { recursive: true });
|
|
133
138
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
134
139
|
|
|
135
|
-
console.log(`Listener started (PID: ${child.pid})
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
console.log(`Listener started (PID: ${child.pid})
|
|
141
|
+
Log: ${logFile}
|
|
142
|
+
Projects: ${Object.keys(config.listener.projects).join(', ')}`);
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
function stopDaemon () {
|
|
@@ -191,8 +196,8 @@ function showStatus () {
|
|
|
191
196
|
}
|
|
192
197
|
|
|
193
198
|
const logFile = getLogFile();
|
|
194
|
-
console.log(`Status: running (PID: ${pid})
|
|
195
|
-
|
|
199
|
+
console.log(`Status: running (PID: ${pid})
|
|
200
|
+
Log: ${logFile}`);
|
|
196
201
|
|
|
197
202
|
// Show last few log lines
|
|
198
203
|
try {
|
|
@@ -266,3 +271,75 @@ function isProcessAlive (pid) {
|
|
|
266
271
|
return false;
|
|
267
272
|
}
|
|
268
273
|
}
|
|
274
|
+
|
|
275
|
+
function ask (rl, question) {
|
|
276
|
+
return new Promise((resolve) => {
|
|
277
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function setupListener () {
|
|
282
|
+
let config = {};
|
|
283
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
284
|
+
try {
|
|
285
|
+
config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
286
|
+
} catch {
|
|
287
|
+
// ignore
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
config.listener = config.listener || {};
|
|
292
|
+
const L = config.listener;
|
|
293
|
+
const home = os.homedir();
|
|
294
|
+
|
|
295
|
+
const defaults = {
|
|
296
|
+
worktreeBaseDir: L.worktreeBaseDir || path.join(home, '.claude', 'worktrees'),
|
|
297
|
+
taskTimeoutMinutes: L.taskTimeoutMinutes ?? 30,
|
|
298
|
+
maxQueuePerWorkDir: L.maxQueuePerWorkDir ?? 10,
|
|
299
|
+
maxTotalTasks: L.maxTotalTasks ?? 50,
|
|
300
|
+
logDir: L.logDir || '',
|
|
301
|
+
taskLogDir: L.taskLogDir || '',
|
|
302
|
+
projectPath: L.projects?.default?.path || '',
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const rl = readline.createInterface({
|
|
306
|
+
input: process.stdin,
|
|
307
|
+
output: process.stdout,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
console.log(`
|
|
311
|
+
Listener Setup
|
|
312
|
+
==============
|
|
313
|
+
Press Enter to keep current value shown in [brackets].
|
|
314
|
+
`);
|
|
315
|
+
|
|
316
|
+
const worktreeBaseDir = await ask(rl, `Worktree base dir [${defaults.worktreeBaseDir}]: `) || defaults.worktreeBaseDir;
|
|
317
|
+
const taskTimeoutStr = await ask(rl, `Task timeout, minutes [${defaults.taskTimeoutMinutes}]: `) || String(defaults.taskTimeoutMinutes);
|
|
318
|
+
const maxQueueStr = await ask(rl, `Max queue per work dir [${defaults.maxQueuePerWorkDir}]: `) || String(defaults.maxQueuePerWorkDir);
|
|
319
|
+
const maxTotalStr = await ask(rl, `Max total tasks [${defaults.maxTotalTasks}]: `) || String(defaults.maxTotalTasks);
|
|
320
|
+
const logDir = await ask(rl, `Log dir [${defaults.logDir || '(default)'}]: `) || defaults.logDir;
|
|
321
|
+
const taskLogDir = await ask(rl, `Task log dir [${defaults.taskLogDir || '(default)'}]: `) || defaults.taskLogDir;
|
|
322
|
+
const projectPath = await ask(rl, `Default project path [${defaults.projectPath || '(none)'}]: `) || defaults.projectPath;
|
|
323
|
+
|
|
324
|
+
rl.close();
|
|
325
|
+
|
|
326
|
+
L.worktreeBaseDir = worktreeBaseDir;
|
|
327
|
+
L.taskTimeoutMinutes = parseInt(taskTimeoutStr, 10) || defaults.taskTimeoutMinutes;
|
|
328
|
+
L.maxQueuePerWorkDir = parseInt(maxQueueStr, 10) || defaults.maxQueuePerWorkDir;
|
|
329
|
+
L.maxTotalTasks = parseInt(maxTotalStr, 10) || defaults.maxTotalTasks;
|
|
330
|
+
L.logDir = logDir;
|
|
331
|
+
L.taskLogDir = taskLogDir;
|
|
332
|
+
|
|
333
|
+
if (projectPath) {
|
|
334
|
+
L.projects = L.projects || {};
|
|
335
|
+
L.projects.default = L.projects.default || {};
|
|
336
|
+
L.projects.default.path = projectPath;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
340
|
+
|
|
341
|
+
console.log(`
|
|
342
|
+
Listener config saved to ${CONFIG_FILE}
|
|
343
|
+
Run "claude-notify listener start" to apply.
|
|
344
|
+
`);
|
|
345
|
+
}
|
package/bin/uninstall.js
CHANGED
|
@@ -9,26 +9,48 @@ const home = os.homedir();
|
|
|
9
9
|
const claudeDir = path.join(home, '.claude');
|
|
10
10
|
const configPath = path.join(claudeDir, 'notifier.config.json');
|
|
11
11
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
12
|
-
const statePath = path.join(claudeDir, '.notifier_state.json');
|
|
13
|
-
|
|
14
|
-
const HOOK_COMMAND = 'claude-notify';
|
|
15
|
-
const PLUGIN_KEY = 'claude-notification-plugin@bazilio-plugins';
|
|
16
|
-
const MARKETPLACE_KEY = 'bazilio-plugins';
|
|
12
|
+
const statePath = path.join(claudeDir, '.notifier_state.json');
|
|
13
|
+
|
|
14
|
+
const HOOK_COMMAND = 'claude-notify';
|
|
15
|
+
const PLUGIN_KEY = 'claude-notification-plugin@bazilio-plugins';
|
|
16
|
+
const MARKETPLACE_KEY = 'bazilio-plugins';
|
|
17
|
+
|
|
18
|
+
function isPluginHookCommand (command) {
|
|
19
|
+
if (typeof command !== 'string') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalized = command.trim().toLowerCase();
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return normalized === HOOK_COMMAND || normalized.startsWith(`${HOOK_COMMAND} `);
|
|
29
|
+
}
|
|
17
30
|
|
|
18
31
|
// Remove hooks from settings.json
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
let hooksRemoved = false;
|
|
33
|
+
let hooksRemoveError = '';
|
|
34
|
+
if (fs.existsSync(settingsPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
37
|
+
let hadPluginHooks = false;
|
|
38
|
+
|
|
39
|
+
if (settings.hooks) {
|
|
40
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
41
|
+
const eventHooks = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
|
|
42
|
+
const hadInEvent = eventHooks.some((matcher) =>
|
|
43
|
+
matcher.hooks?.some((h) => isPluginHookCommand(h.command)),
|
|
44
|
+
);
|
|
45
|
+
hadPluginHooks = hadPluginHooks || hadInEvent;
|
|
46
|
+
|
|
47
|
+
settings.hooks[event] = settings.hooks[event].filter((matcher) =>
|
|
48
|
+
!matcher.hooks?.some((h) => isPluginHookCommand(h.command)),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (settings.hooks[event].length === 0) {
|
|
52
|
+
delete settings.hooks[event];
|
|
53
|
+
}
|
|
32
54
|
}
|
|
33
55
|
|
|
34
56
|
if (Object.keys(settings.hooks).length === 0) {
|
|
@@ -53,9 +75,22 @@ if (fs.existsSync(settingsPath)) {
|
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
78
|
+
|
|
79
|
+
// Verify hooks were actually removed
|
|
80
|
+
const verify = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
81
|
+
const remainingPluginHooks = verify.hooks
|
|
82
|
+
? Object.values(verify.hooks).some((matchers) =>
|
|
83
|
+
Array.isArray(matchers) &&
|
|
84
|
+
matchers.some((m) => m.hooks?.some((h) => isPluginHookCommand(h.command))),
|
|
85
|
+
)
|
|
86
|
+
: false;
|
|
87
|
+
hooksRemoved = hadPluginHooks && !remainingPluginHooks;
|
|
88
|
+
if (hadPluginHooks && remainingPluginHooks) {
|
|
89
|
+
hooksRemoveError = 'Hooks still present in settings.json after removal attempt';
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
hooksRemoveError = `Failed to update settings.json: ${err.message}`;
|
|
93
|
+
}
|
|
59
94
|
}
|
|
60
95
|
|
|
61
96
|
// Remove config, state, and resolver files
|
|
@@ -66,9 +101,8 @@ for (const file of [configPath, statePath, resolverPath]) {
|
|
|
66
101
|
}
|
|
67
102
|
}
|
|
68
103
|
|
|
69
|
-
// Remove CLI wrapper
|
|
70
|
-
|
|
71
|
-
const WRAPPER_NAMES = ['claude-notify', 'claude-notify-listener', 'claude-notify-install', 'claude-notify-uninstall'];
|
|
104
|
+
// Remove CLI wrapper script
|
|
105
|
+
const WRAPPER_NAMES = ['claude-notify'];
|
|
72
106
|
let cliBinsRemoved = false;
|
|
73
107
|
const ext = process.platform === 'win32' ? '.cmd' : '';
|
|
74
108
|
|
|
@@ -121,6 +155,12 @@ if (fs.existsSync(pluginCacheDir)) {
|
|
|
121
155
|
cacheRemoved = !fs.existsSync(pluginCacheDir);
|
|
122
156
|
}
|
|
123
157
|
|
|
158
|
+
const hooksStatus = hooksRemoved
|
|
159
|
+
? 'Hooks removed from settings.json'
|
|
160
|
+
: hooksRemoveError
|
|
161
|
+
? `Warning: ${hooksRemoveError}`
|
|
162
|
+
: 'No hooks found in settings.json';
|
|
163
|
+
|
|
124
164
|
const extras = [
|
|
125
165
|
pluginEntryRemoved || cacheRemoved ? 'Plugin registration cleaned.' : '',
|
|
126
166
|
cliBinsRemoved ? 'CLI wrapper scripts removed.' : '',
|
|
@@ -132,13 +172,13 @@ if (cacheStillExists) {
|
|
|
132
172
|
Warning: Could not fully remove plugin cache directory:
|
|
133
173
|
${pluginCacheDir}
|
|
134
174
|
Please remove it manually.
|
|
135
|
-
|
|
175
|
+
${hooksStatus}
|
|
136
176
|
Config files deleted.${extras ? `\n${extras}` : ''}
|
|
137
177
|
`);
|
|
138
178
|
} else {
|
|
139
179
|
console.log(`
|
|
140
180
|
Claude Notification Plugin uninstalled.
|
|
141
|
-
|
|
181
|
+
${hooksStatus}
|
|
142
182
|
Config files deleted.${extras ? `\n${extras}` : ''}
|
|
143
183
|
`);
|
|
144
184
|
}
|
package/commit-sha
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
e766d03df0c9561a16f5f2e1dff3dc896c1ee862
|
|
@@ -276,6 +276,26 @@ TaskRunner emit 'complete'/'error'/'timeout'
|
|
|
276
276
|
|
|
277
277
|
## Configuration
|
|
278
278
|
|
|
279
|
+
### Interactive setup
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
claude-notify listener setup
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Prompts for each listener setting interactively. Current values are shown in `[brackets]` — press Enter to keep them:
|
|
286
|
+
|
|
287
|
+
- **Worktree base dir** — where auto-created worktrees are stored
|
|
288
|
+
- **Task timeout, minutes** — max execution time per task
|
|
289
|
+
- **Max queue per work dir** — queue limit per working directory
|
|
290
|
+
- **Max total tasks** — global task limit across all queues
|
|
291
|
+
- **Log dir** — listener operational log directory
|
|
292
|
+
- **Task log dir** — task Q&A log directory
|
|
293
|
+
- **Default project path** — path for the `default` project alias
|
|
294
|
+
|
|
295
|
+
Re-run `claude-notify listener setup` anytime to reconfigure.
|
|
296
|
+
|
|
297
|
+
### Manual configuration
|
|
298
|
+
|
|
279
299
|
Full example of `~/.claude/notifier.config.json` with the listener section:
|
|
280
300
|
|
|
281
301
|
```json
|
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.
|
|
4
|
+
"version": "1.0.101",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|