claude-notification-plugin 1.1.6 → 1.1.10
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/README.md +1 -1
- package/bin/constants.js +33 -0
- package/bin/install.js +42 -49
- package/bin/listener-cli.js +22 -26
- package/bin/uninstall.js +43 -75
- package/commit-sha +1 -1
- package/listener/LISTENER-DETAILED.md +2 -2
- package/listener/listener.js +3 -4
- package/notifier/notifier.js +10 -18
- package/package.json +3 -2
package/README.md
CHANGED
package/bin/constants.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export const HOME = os.homedir();
|
|
5
|
+
export const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
6
|
+
export const PLUGINS_DIR = path.join(CLAUDE_DIR, 'plugins');
|
|
7
|
+
|
|
8
|
+
// File names
|
|
9
|
+
export const CONFIG_FILENAME = 'claude-notify.config.json';
|
|
10
|
+
export const STATE_FILENAME = '.notifier_state.json';
|
|
11
|
+
export const PID_FILENAME = '.listener.pid';
|
|
12
|
+
export const RESOLVER_FILENAME = 'claude-notify-resolve.js';
|
|
13
|
+
export const LISTENER_LOG_FILENAME = '.cc-n-listener.log';
|
|
14
|
+
export const INSTALL_LOG_FILENAME = 'claude-notify-install.log';
|
|
15
|
+
|
|
16
|
+
// Full paths
|
|
17
|
+
export const CONFIG_PATH = path.join(CLAUDE_DIR, CONFIG_FILENAME);
|
|
18
|
+
export const STATE_PATH = path.join(CLAUDE_DIR, STATE_FILENAME);
|
|
19
|
+
export const PID_PATH = path.join(CLAUDE_DIR, PID_FILENAME);
|
|
20
|
+
export const RESOLVER_PATH = path.join(CLAUDE_DIR, RESOLVER_FILENAME);
|
|
21
|
+
export const LISTENER_LOG_PATH = path.join(CLAUDE_DIR, LISTENER_LOG_FILENAME);
|
|
22
|
+
export const INSTALL_LOG_PATH = path.join(CLAUDE_DIR, INSTALL_LOG_FILENAME);
|
|
23
|
+
export const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
|
|
24
|
+
export const INSTALLED_PLUGINS_PATH = path.join(PLUGINS_DIR, 'installed_plugins.json');
|
|
25
|
+
export const KNOWN_MARKETPLACES_PATH = path.join(PLUGINS_DIR, 'known_marketplaces.json');
|
|
26
|
+
export const MARKETPLACES_DIR = path.join(PLUGINS_DIR, 'marketplaces');
|
|
27
|
+
|
|
28
|
+
// Plugin identity
|
|
29
|
+
export const HOOK_COMMAND = 'claude-notify';
|
|
30
|
+
export const MARKETPLACE_KEY = 'bazilio-plugins';
|
|
31
|
+
export const PLUGIN_KEY = 'claude-notification-plugin@bazilio-plugins';
|
|
32
|
+
export const MARKETPLACE_REPO = 'https://github.com/Bazilio-san/claude-plugins.git';
|
|
33
|
+
export const MARKETPLACE_GITHUB = 'Bazilio-san/claude-plugins';
|
package/bin/install.js
CHANGED
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
import os from 'os';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import readline from 'readline';
|
|
7
6
|
import { execSync } from 'child_process';
|
|
8
7
|
import { fileURLToPath } from 'url';
|
|
8
|
+
import {
|
|
9
|
+
HOME, CLAUDE_DIR, PLUGINS_DIR,
|
|
10
|
+
CONFIG_PATH, PID_PATH, RESOLVER_PATH, INSTALL_LOG_PATH,
|
|
11
|
+
SETTINGS_PATH, INSTALLED_PLUGINS_PATH, KNOWN_MARKETPLACES_PATH, MARKETPLACES_DIR,
|
|
12
|
+
HOOK_COMMAND, MARKETPLACE_KEY, PLUGIN_KEY, MARKETPLACE_REPO, MARKETPLACE_GITHUB,
|
|
13
|
+
} from './constants.js';
|
|
9
14
|
|
|
10
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
16
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
12
17
|
|
|
13
|
-
const home = os.homedir();
|
|
14
|
-
const claudeDir = path.join(home, '.claude');
|
|
15
|
-
const pluginsDir = path.join(claudeDir, 'plugins');
|
|
16
|
-
const configPath = path.join(claudeDir, 'notifier.config.json');
|
|
17
|
-
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
18
|
-
const installedPluginsPath = path.join(pluginsDir, 'installed_plugins.json');
|
|
19
|
-
const knownMarketplacesPath = path.join(pluginsDir, 'known_marketplaces.json');
|
|
20
|
-
const marketplacesDir = path.join(pluginsDir, 'marketplaces');
|
|
21
|
-
const RESOLVER_PATH = path.join(claudeDir, 'claude-notify-resolve.js');
|
|
22
|
-
const pidFile = path.join(claudeDir, '.listener.pid');
|
|
23
|
-
const installLogPath = path.join(claudeDir, 'claude-notify-install.log');
|
|
24
|
-
|
|
25
18
|
// ──────────────────────────────────────
|
|
26
19
|
// Logging to file
|
|
27
20
|
// ──────────────────────────────────────
|
|
@@ -29,8 +22,8 @@ const installLogPath = path.join(claudeDir, 'claude-notify-install.log');
|
|
|
29
22
|
let logStream;
|
|
30
23
|
|
|
31
24
|
function initLog () {
|
|
32
|
-
fs.mkdirSync(
|
|
33
|
-
logStream = fs.createWriteStream(
|
|
25
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
26
|
+
logStream = fs.createWriteStream(INSTALL_LOG_PATH, { flags: 'w' });
|
|
34
27
|
const origLog = console.log.bind(console);
|
|
35
28
|
const origWarn = console.warn.bind(console);
|
|
36
29
|
const stamp = () => new Date().toISOString();
|
|
@@ -107,10 +100,10 @@ function stopListenerIfRunning () {
|
|
|
107
100
|
let stopped = false;
|
|
108
101
|
const pidFromFile = (() => {
|
|
109
102
|
try {
|
|
110
|
-
if (!fs.existsSync(
|
|
103
|
+
if (!fs.existsSync(PID_PATH)) {
|
|
111
104
|
return null;
|
|
112
105
|
}
|
|
113
|
-
const pid = parseInt(fs.readFileSync(
|
|
106
|
+
const pid = parseInt(fs.readFileSync(PID_PATH, 'utf-8').trim(), 10);
|
|
114
107
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
115
108
|
} catch {
|
|
116
109
|
return null;
|
|
@@ -122,20 +115,14 @@ function stopListenerIfRunning () {
|
|
|
122
115
|
}
|
|
123
116
|
|
|
124
117
|
try {
|
|
125
|
-
if (fs.existsSync(
|
|
126
|
-
fs.unlinkSync(
|
|
118
|
+
if (fs.existsSync(PID_PATH)) {
|
|
119
|
+
fs.unlinkSync(PID_PATH);
|
|
127
120
|
}
|
|
128
121
|
} catch { /* ignore */ }
|
|
129
122
|
|
|
130
123
|
return stopped;
|
|
131
124
|
}
|
|
132
125
|
|
|
133
|
-
const HOOK_COMMAND = 'claude-notify';
|
|
134
|
-
const MARKETPLACE_KEY = 'bazilio-plugins';
|
|
135
|
-
const PLUGIN_KEY = 'claude-notification-plugin@bazilio-plugins';
|
|
136
|
-
const MARKETPLACE_REPO = 'https://github.com/Bazilio-san/claude-plugins.git';
|
|
137
|
-
const MARKETPLACE_GITHUB = 'Bazilio-san/claude-plugins';
|
|
138
|
-
|
|
139
126
|
const CLI_BIN_NAME = 'claude-notify';
|
|
140
127
|
const CLI_BIN_TARGET = 'bin/cli.js';
|
|
141
128
|
|
|
@@ -158,7 +145,7 @@ function readCommitSha () {
|
|
|
158
145
|
}
|
|
159
146
|
|
|
160
147
|
function copyToCache (version) {
|
|
161
|
-
const cacheBase = path.join(
|
|
148
|
+
const cacheBase = path.join(PLUGINS_DIR, 'cache', MARKETPLACE_KEY, 'claude-notification-plugin');
|
|
162
149
|
const dest = path.join(cacheBase, version);
|
|
163
150
|
|
|
164
151
|
if (fs.existsSync(dest)) {
|
|
@@ -203,12 +190,12 @@ function copyToCache (version) {
|
|
|
203
190
|
}
|
|
204
191
|
|
|
205
192
|
function updateInstalledPlugins (version, installPath) {
|
|
206
|
-
fs.mkdirSync(
|
|
193
|
+
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
207
194
|
|
|
208
195
|
let data = { version: 2, plugins: {} };
|
|
209
|
-
if (fs.existsSync(
|
|
196
|
+
if (fs.existsSync(INSTALLED_PLUGINS_PATH)) {
|
|
210
197
|
try {
|
|
211
|
-
data = JSON.parse(fs.readFileSync(
|
|
198
|
+
data = JSON.parse(fs.readFileSync(INSTALLED_PLUGINS_PATH, 'utf-8'));
|
|
212
199
|
} catch {
|
|
213
200
|
// ignore malformed file
|
|
214
201
|
}
|
|
@@ -226,20 +213,20 @@ function updateInstalledPlugins (version, installPath) {
|
|
|
226
213
|
gitCommitSha: readCommitSha(),
|
|
227
214
|
}];
|
|
228
215
|
|
|
229
|
-
fs.writeFileSync(
|
|
216
|
+
fs.writeFileSync(INSTALLED_PLUGINS_PATH, JSON.stringify(data, null, 2));
|
|
230
217
|
}
|
|
231
218
|
|
|
232
219
|
function updateKnownMarketplaces () {
|
|
233
220
|
let data = {};
|
|
234
|
-
if (fs.existsSync(
|
|
221
|
+
if (fs.existsSync(KNOWN_MARKETPLACES_PATH)) {
|
|
235
222
|
try {
|
|
236
|
-
data = JSON.parse(fs.readFileSync(
|
|
223
|
+
data = JSON.parse(fs.readFileSync(KNOWN_MARKETPLACES_PATH, 'utf-8'));
|
|
237
224
|
} catch {
|
|
238
225
|
// ignore malformed file
|
|
239
226
|
}
|
|
240
227
|
}
|
|
241
228
|
|
|
242
|
-
const installLocation = path.join(
|
|
229
|
+
const installLocation = path.join(MARKETPLACES_DIR, MARKETPLACE_KEY);
|
|
243
230
|
|
|
244
231
|
data[MARKETPLACE_KEY] = {
|
|
245
232
|
...data[MARKETPLACE_KEY],
|
|
@@ -252,11 +239,11 @@ function updateKnownMarketplaces () {
|
|
|
252
239
|
autoUpdate: true,
|
|
253
240
|
};
|
|
254
241
|
|
|
255
|
-
fs.writeFileSync(
|
|
242
|
+
fs.writeFileSync(KNOWN_MARKETPLACES_PATH, JSON.stringify(data, null, 2));
|
|
256
243
|
}
|
|
257
244
|
|
|
258
245
|
function cloneOrUpdateMarketplace () {
|
|
259
|
-
const dest = path.join(
|
|
246
|
+
const dest = path.join(MARKETPLACES_DIR, MARKETPLACE_KEY);
|
|
260
247
|
|
|
261
248
|
if (fs.existsSync(path.join(dest, '.git'))) {
|
|
262
249
|
try {
|
|
@@ -265,7 +252,7 @@ function cloneOrUpdateMarketplace () {
|
|
|
265
252
|
// offline or conflict — not fatal
|
|
266
253
|
}
|
|
267
254
|
} else {
|
|
268
|
-
fs.mkdirSync(
|
|
255
|
+
fs.mkdirSync(MARKETPLACES_DIR, { recursive: true });
|
|
269
256
|
try {
|
|
270
257
|
execSync(`git clone "${MARKETPLACE_REPO}" "${dest}"`, {
|
|
271
258
|
stdio: 'pipe',
|
|
@@ -423,6 +410,9 @@ async function main () {
|
|
|
423
410
|
|
|
424
411
|
// 0. Stop listener if running (before overwriting files)
|
|
425
412
|
const listenerWasStopped = stopListenerIfRunning();
|
|
413
|
+
if (listenerWasStopped) {
|
|
414
|
+
console.log(' Listener daemon stopped');
|
|
415
|
+
}
|
|
426
416
|
|
|
427
417
|
// 1. Register plugin in Claude Code
|
|
428
418
|
const version = getVersion();
|
|
@@ -448,9 +438,9 @@ async function main () {
|
|
|
448
438
|
|
|
449
439
|
// 2. Interactive Telegram setup
|
|
450
440
|
let existing = {};
|
|
451
|
-
if (fs.existsSync(
|
|
441
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
452
442
|
try {
|
|
453
|
-
existing = JSON.parse(fs.readFileSync(
|
|
443
|
+
existing = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
454
444
|
} catch {
|
|
455
445
|
// ignore malformed config
|
|
456
446
|
}
|
|
@@ -519,7 +509,7 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
519
509
|
}
|
|
520
510
|
|
|
521
511
|
// 3. Write config
|
|
522
|
-
fs.mkdirSync(
|
|
512
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
523
513
|
|
|
524
514
|
const platform = process.platform;
|
|
525
515
|
let defaultSoundFile;
|
|
@@ -553,7 +543,7 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
553
543
|
debug: false,
|
|
554
544
|
listener: {
|
|
555
545
|
projects: {},
|
|
556
|
-
worktreeBaseDir: path.join(
|
|
546
|
+
worktreeBaseDir: path.join(HOME, '.claude', 'worktrees'),
|
|
557
547
|
autoCreateWorktree: true,
|
|
558
548
|
taskTimeoutMinutes: 30,
|
|
559
549
|
maxQueuePerWorkDir: 10,
|
|
@@ -577,14 +567,15 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
577
567
|
config.telegram.chatId = chatId;
|
|
578
568
|
}
|
|
579
569
|
|
|
580
|
-
fs.writeFileSync(
|
|
570
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
571
|
+
console.log(` Config saved: ${CONFIG_PATH}`);
|
|
581
572
|
|
|
582
573
|
// 4. Register hooks
|
|
583
574
|
let settings = {};
|
|
584
575
|
|
|
585
|
-
if (fs.existsSync(
|
|
576
|
+
if (fs.existsSync(SETTINGS_PATH)) {
|
|
586
577
|
try {
|
|
587
|
-
settings = JSON.parse(fs.readFileSync(
|
|
578
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
588
579
|
} catch {
|
|
589
580
|
settings = {};
|
|
590
581
|
}
|
|
@@ -593,6 +584,7 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
593
584
|
// Register plugin as enabled
|
|
594
585
|
settings.enabledPlugins = settings.enabledPlugins || {};
|
|
595
586
|
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
587
|
+
console.log(' Registered plugin in enabledPlugins');
|
|
596
588
|
|
|
597
589
|
// When the plugin is enabled, Claude Code loads hooks from hooks/hooks.json automatically.
|
|
598
590
|
// Remove any duplicate hooks from settings.json to avoid double notifications.
|
|
@@ -600,6 +592,7 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
600
592
|
removeHook(settings, 'UserPromptSubmit');
|
|
601
593
|
removeHook(settings, 'Stop');
|
|
602
594
|
removeHook(settings, 'Notification');
|
|
595
|
+
console.log(' Cleaned duplicate hooks from settings.json');
|
|
603
596
|
|
|
604
597
|
// Register marketplace
|
|
605
598
|
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
|
|
@@ -609,8 +602,10 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
609
602
|
repo: MARKETPLACE_GITHUB,
|
|
610
603
|
},
|
|
611
604
|
};
|
|
605
|
+
console.log(' Registered marketplace in extraKnownMarketplaces');
|
|
612
606
|
|
|
613
|
-
fs.writeFileSync(
|
|
607
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
608
|
+
console.log(` Settings saved: ${SETTINGS_PATH}`);
|
|
614
609
|
|
|
615
610
|
// 5. Summary
|
|
616
611
|
const telegramStatus = config.telegram.token && config.telegram.chatId
|
|
@@ -626,20 +621,18 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
626
621
|
sudo apt install espeak`;
|
|
627
622
|
}
|
|
628
623
|
|
|
629
|
-
const listenerLine = listenerWasStopped ? '\nListener was stopped (restart manually if needed).' : '';
|
|
630
|
-
|
|
631
624
|
console.log(`
|
|
632
625
|
Installed!
|
|
633
|
-
|
|
626
|
+
|
|
634
627
|
Plugin hooks (via hooks/hooks.json):
|
|
635
628
|
- UserPromptSubmit (start timer)
|
|
636
629
|
- Stop (task finished)
|
|
637
630
|
- Notification (waiting for input)
|
|
638
631
|
|
|
639
|
-
Config: ${
|
|
632
|
+
Config: ${CONFIG_PATH}
|
|
640
633
|
${telegramStatus}${platformTip}
|
|
641
634
|
|
|
642
|
-
Log: ${
|
|
635
|
+
Log: ${INSTALL_LOG_PATH}
|
|
643
636
|
|
|
644
637
|
To uninstall: claude-notify uninstall
|
|
645
638
|
|
package/bin/listener-cli.js
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
import os from 'os';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import readline from 'readline';
|
|
7
6
|
import { spawn, execSync } from 'child_process';
|
|
8
7
|
import { fileURLToPath } from 'url';
|
|
8
|
+
import {
|
|
9
|
+
HOME, CLAUDE_DIR, CONFIG_PATH, PID_PATH, LISTENER_LOG_FILENAME,
|
|
10
|
+
} from './constants.js';
|
|
9
11
|
|
|
10
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
13
|
const __dirname = path.dirname(__filename);
|
|
12
14
|
|
|
13
|
-
const DEFAULT_LOG_DIR = path.join(os.homedir(), '.claude');
|
|
14
|
-
const PID_FILE = path.join(os.homedir(), '.claude', '.listener.pid');
|
|
15
|
-
const CONFIG_FILE = path.join(os.homedir(), '.claude', 'notifier.config.json');
|
|
16
|
-
|
|
17
15
|
function getLogFile () {
|
|
18
16
|
try {
|
|
19
|
-
const cfg = JSON.parse(fs.readFileSync(
|
|
20
|
-
const logDir = cfg.listener?.logDir ||
|
|
21
|
-
return path.join(logDir,
|
|
17
|
+
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
18
|
+
const logDir = cfg.listener?.logDir || CLAUDE_DIR;
|
|
19
|
+
return path.join(logDir, LISTENER_LOG_FILENAME);
|
|
22
20
|
} catch {
|
|
23
|
-
return path.join(
|
|
21
|
+
return path.join(CLAUDE_DIR, LISTENER_LOG_FILENAME);
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
24
|
const LISTENER_SCRIPT = path.join(__dirname, '..', 'listener', 'listener.js');
|
|
@@ -71,22 +69,22 @@ function startDaemon () {
|
|
|
71
69
|
// Clean stale PID file
|
|
72
70
|
if (existingPid) {
|
|
73
71
|
try {
|
|
74
|
-
fs.unlinkSync(
|
|
72
|
+
fs.unlinkSync(PID_PATH);
|
|
75
73
|
} catch {
|
|
76
74
|
// ignore
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
// Validate config
|
|
81
|
-
if (!fs.existsSync(
|
|
82
|
-
console.error(`Config not found: ${
|
|
79
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
80
|
+
console.error(`Config not found: ${CONFIG_PATH}`);
|
|
83
81
|
console.error('Run claude-notify install first, or create the config manually.');
|
|
84
82
|
process.exit(1);
|
|
85
83
|
}
|
|
86
84
|
|
|
87
85
|
let config;
|
|
88
86
|
try {
|
|
89
|
-
config = JSON.parse(fs.readFileSync(
|
|
87
|
+
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
90
88
|
} catch (err) {
|
|
91
89
|
console.error(`Invalid config: ${err.message}`);
|
|
92
90
|
process.exit(1);
|
|
@@ -134,8 +132,8 @@ function startDaemon () {
|
|
|
134
132
|
fs.closeSync(logFd);
|
|
135
133
|
|
|
136
134
|
// Write PID
|
|
137
|
-
fs.mkdirSync(path.dirname(
|
|
138
|
-
fs.writeFileSync(
|
|
135
|
+
fs.mkdirSync(path.dirname(PID_PATH), { recursive: true });
|
|
136
|
+
fs.writeFileSync(PID_PATH, String(child.pid));
|
|
139
137
|
|
|
140
138
|
console.log(`Listener started (PID: ${child.pid})
|
|
141
139
|
Log: ${logFile}
|
|
@@ -235,8 +233,8 @@ function showLogs () {
|
|
|
235
233
|
|
|
236
234
|
function readPid () {
|
|
237
235
|
try {
|
|
238
|
-
if (fs.existsSync(
|
|
239
|
-
const pid = parseInt(fs.readFileSync(
|
|
236
|
+
if (fs.existsSync(PID_PATH)) {
|
|
237
|
+
const pid = parseInt(fs.readFileSync(PID_PATH, 'utf-8').trim(), 10);
|
|
240
238
|
return isNaN(pid) ? null : pid;
|
|
241
239
|
}
|
|
242
240
|
} catch {
|
|
@@ -247,8 +245,8 @@ function readPid () {
|
|
|
247
245
|
|
|
248
246
|
function cleanPid () {
|
|
249
247
|
try {
|
|
250
|
-
if (fs.existsSync(
|
|
251
|
-
fs.unlinkSync(
|
|
248
|
+
if (fs.existsSync(PID_PATH)) {
|
|
249
|
+
fs.unlinkSync(PID_PATH);
|
|
252
250
|
}
|
|
253
251
|
} catch {
|
|
254
252
|
// ignore
|
|
@@ -365,9 +363,9 @@ async function validateProjectPath (rl, inputPath) {
|
|
|
365
363
|
|
|
366
364
|
async function setupListener () {
|
|
367
365
|
let config = {};
|
|
368
|
-
if (fs.existsSync(
|
|
366
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
369
367
|
try {
|
|
370
|
-
config = JSON.parse(fs.readFileSync(
|
|
368
|
+
config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
371
369
|
} catch {
|
|
372
370
|
// ignore
|
|
373
371
|
}
|
|
@@ -375,10 +373,8 @@ async function setupListener () {
|
|
|
375
373
|
|
|
376
374
|
config.listener = config.listener || {};
|
|
377
375
|
const L = config.listener;
|
|
378
|
-
const home = os.homedir();
|
|
379
|
-
|
|
380
376
|
const defaults = {
|
|
381
|
-
worktreeBaseDir: L.worktreeBaseDir || path.join(
|
|
377
|
+
worktreeBaseDir: L.worktreeBaseDir || path.join(HOME, '.claude', 'worktrees'),
|
|
382
378
|
taskTimeoutMinutes: L.taskTimeoutMinutes ?? 30,
|
|
383
379
|
maxQueuePerWorkDir: L.maxQueuePerWorkDir ?? 10,
|
|
384
380
|
maxTotalTasks: L.maxTotalTasks ?? 50,
|
|
@@ -497,10 +493,10 @@ Press Enter to keep current value shown in [brackets].
|
|
|
497
493
|
|
|
498
494
|
rl.close();
|
|
499
495
|
|
|
500
|
-
fs.writeFileSync(
|
|
496
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
501
497
|
|
|
502
498
|
const projectCount = Object.keys(L.projects).length;
|
|
503
|
-
console.log(`\nListener config saved to ${
|
|
499
|
+
console.log(`\nListener config saved to ${CONFIG_PATH}`);
|
|
504
500
|
if (hasValidProject || projectCount > 0) {
|
|
505
501
|
console.log('Run "claude-notify listener start" to apply.');
|
|
506
502
|
} else {
|
package/bin/uninstall.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
import os from 'os';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import { execSync } from 'child_process';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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';
|
|
6
|
+
import {
|
|
7
|
+
HOME, CLAUDE_DIR,
|
|
8
|
+
CONFIG_PATH, STATE_PATH, PID_PATH, RESOLVER_PATH, LISTENER_LOG_PATH,
|
|
9
|
+
SETTINGS_PATH, INSTALLED_PLUGINS_PATH, KNOWN_MARKETPLACES_PATH,
|
|
10
|
+
HOOK_COMMAND, PLUGIN_KEY, MARKETPLACE_KEY,
|
|
11
|
+
} from './constants.js';
|
|
18
12
|
|
|
19
13
|
function isPluginHookCommand (command) {
|
|
20
14
|
if (typeof command !== 'string') {
|
|
@@ -155,7 +149,7 @@ function stopListenerIfRunning () {
|
|
|
155
149
|
let stopped = false;
|
|
156
150
|
const processed = new Set();
|
|
157
151
|
|
|
158
|
-
const pidFromFile = readPid(
|
|
152
|
+
const pidFromFile = readPid(PID_PATH);
|
|
159
153
|
if (pidFromFile) {
|
|
160
154
|
if (killProcessTree(pidFromFile)) {
|
|
161
155
|
stopped = true;
|
|
@@ -172,19 +166,19 @@ function stopListenerIfRunning () {
|
|
|
172
166
|
}
|
|
173
167
|
}
|
|
174
168
|
|
|
175
|
-
removeFileIfExists(
|
|
169
|
+
removeFileIfExists(PID_PATH);
|
|
176
170
|
return stopped;
|
|
177
171
|
}
|
|
178
172
|
|
|
173
|
+
console.log('\nUninstalling Claude Notification Plugin...\n');
|
|
174
|
+
|
|
179
175
|
// Stop listener daemon if running
|
|
180
|
-
|
|
176
|
+
stopListenerIfRunning();
|
|
181
177
|
|
|
182
178
|
// Remove hooks from settings.json
|
|
183
|
-
|
|
184
|
-
let hooksRemoveError = '';
|
|
185
|
-
if (fs.existsSync(settingsPath)) {
|
|
179
|
+
if (fs.existsSync(SETTINGS_PATH)) {
|
|
186
180
|
try {
|
|
187
|
-
const settings = JSON.parse(fs.readFileSync(
|
|
181
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
188
182
|
let hadPluginHooks = false;
|
|
189
183
|
|
|
190
184
|
if (settings.hooks) {
|
|
@@ -215,6 +209,7 @@ if (fs.existsSync(settingsPath)) {
|
|
|
215
209
|
if (Object.keys(settings.enabledPlugins).length === 0) {
|
|
216
210
|
delete settings.enabledPlugins;
|
|
217
211
|
}
|
|
212
|
+
console.log('Removed plugin from enabledPlugins in settings.json');
|
|
218
213
|
}
|
|
219
214
|
|
|
220
215
|
// Remove marketplace from extraKnownMarketplaces
|
|
@@ -223,39 +218,39 @@ if (fs.existsSync(settingsPath)) {
|
|
|
223
218
|
if (Object.keys(settings.extraKnownMarketplaces).length === 0) {
|
|
224
219
|
delete settings.extraKnownMarketplaces;
|
|
225
220
|
}
|
|
221
|
+
console.log('Removed marketplace from extraKnownMarketplaces in settings.json');
|
|
226
222
|
}
|
|
227
223
|
|
|
228
|
-
fs.writeFileSync(
|
|
224
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
229
225
|
|
|
230
226
|
// Verify hooks were actually removed
|
|
231
|
-
const verify = JSON.parse(fs.readFileSync(
|
|
227
|
+
const verify = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
232
228
|
const remainingPluginHooks = verify.hooks
|
|
233
229
|
? Object.values(verify.hooks).some((matchers) =>
|
|
234
230
|
Array.isArray(matchers) &&
|
|
235
231
|
matchers.some((m) => m.hooks?.some((h) => isPluginHookCommand(h.command))),
|
|
236
232
|
)
|
|
237
233
|
: false;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
234
|
+
if (hadPluginHooks && !remainingPluginHooks) {
|
|
235
|
+
console.log('Removed hooks from settings.json');
|
|
236
|
+
} else if (hadPluginHooks && remainingPluginHooks) {
|
|
237
|
+
console.warn('Warning: hooks still present in settings.json after removal attempt');
|
|
241
238
|
}
|
|
242
239
|
} catch (err) {
|
|
243
|
-
|
|
240
|
+
console.error(`Error: failed to update settings.json: ${err.message}`);
|
|
244
241
|
}
|
|
245
242
|
}
|
|
246
243
|
|
|
247
244
|
// Remove config, state, resolver, and listener files
|
|
248
|
-
const
|
|
249
|
-
const listenerLogFile = path.join(claudeDir, '.cc-n-listener.log');
|
|
250
|
-
for (const file of [configPath, statePath, resolverPath, pidFile, listenerLogFile]) {
|
|
245
|
+
for (const file of [CONFIG_PATH, STATE_PATH, RESOLVER_PATH, PID_PATH, LISTENER_LOG_PATH]) {
|
|
251
246
|
if (fs.existsSync(file)) {
|
|
252
247
|
fs.unlinkSync(file);
|
|
248
|
+
console.log(`Removed ${path.basename(file)}`);
|
|
253
249
|
}
|
|
254
250
|
}
|
|
255
251
|
|
|
256
252
|
// Remove CLI wrapper script
|
|
257
253
|
const WRAPPER_NAMES = ['claude-notify'];
|
|
258
|
-
let cliBinsRemoved = false;
|
|
259
254
|
const ext = process.platform === 'win32' ? '.cmd' : '';
|
|
260
255
|
|
|
261
256
|
// Collect directories to check for wrapper scripts
|
|
@@ -271,28 +266,26 @@ try {
|
|
|
271
266
|
}
|
|
272
267
|
|
|
273
268
|
// ~/.local/bin (common on Linux/macOS, also used on Windows)
|
|
274
|
-
wrapperDirs.add(path.join(
|
|
269
|
+
wrapperDirs.add(path.join(HOME, '.local', 'bin'));
|
|
275
270
|
|
|
276
271
|
for (const dir of wrapperDirs) {
|
|
277
272
|
for (const name of WRAPPER_NAMES) {
|
|
278
273
|
const filePath = path.join(dir, `${name}${ext}`);
|
|
279
274
|
if (fs.existsSync(filePath)) {
|
|
280
275
|
fs.unlinkSync(filePath);
|
|
281
|
-
|
|
276
|
+
console.log(`Removed CLI wrapper: ${filePath}`);
|
|
282
277
|
}
|
|
283
278
|
}
|
|
284
279
|
}
|
|
285
280
|
|
|
286
281
|
// Remove from installed_plugins.json
|
|
287
|
-
|
|
288
|
-
let pluginEntryRemoved = false;
|
|
289
|
-
if (fs.existsSync(installedPluginsPath)) {
|
|
282
|
+
if (fs.existsSync(INSTALLED_PLUGINS_PATH)) {
|
|
290
283
|
try {
|
|
291
|
-
const data = JSON.parse(fs.readFileSync(
|
|
284
|
+
const data = JSON.parse(fs.readFileSync(INSTALLED_PLUGINS_PATH, 'utf-8'));
|
|
292
285
|
if (data.plugins?.[PLUGIN_KEY]) {
|
|
293
286
|
delete data.plugins[PLUGIN_KEY];
|
|
294
|
-
fs.writeFileSync(
|
|
295
|
-
|
|
287
|
+
fs.writeFileSync(INSTALLED_PLUGINS_PATH, JSON.stringify(data, null, 2));
|
|
288
|
+
console.log('Removed plugin from installed_plugins.json');
|
|
296
289
|
}
|
|
297
290
|
} catch {
|
|
298
291
|
// ignore
|
|
@@ -300,13 +293,12 @@ if (fs.existsSync(installedPluginsPath)) {
|
|
|
300
293
|
}
|
|
301
294
|
|
|
302
295
|
// Remove marketplace from known_marketplaces.json if no plugins reference it
|
|
303
|
-
const knownMarketplacesPath = path.join(claudeDir, 'plugins', 'known_marketplaces.json');
|
|
304
296
|
try {
|
|
305
|
-
if (fs.existsSync(
|
|
297
|
+
if (fs.existsSync(KNOWN_MARKETPLACES_PATH)) {
|
|
306
298
|
// Check if any remaining plugins reference this marketplace
|
|
307
299
|
let hasMarketplacePlugins = false;
|
|
308
|
-
if (fs.existsSync(
|
|
309
|
-
const data = JSON.parse(fs.readFileSync(
|
|
300
|
+
if (fs.existsSync(INSTALLED_PLUGINS_PATH)) {
|
|
301
|
+
const data = JSON.parse(fs.readFileSync(INSTALLED_PLUGINS_PATH, 'utf-8'));
|
|
310
302
|
if (data.plugins) {
|
|
311
303
|
hasMarketplacePlugins = Object.keys(data.plugins)
|
|
312
304
|
.some((key) => key.endsWith(`@${MARKETPLACE_KEY}`));
|
|
@@ -314,13 +306,13 @@ try {
|
|
|
314
306
|
}
|
|
315
307
|
|
|
316
308
|
if (!hasMarketplacePlugins) {
|
|
317
|
-
const marketplaces = JSON.parse(fs.readFileSync(
|
|
309
|
+
const marketplaces = JSON.parse(fs.readFileSync(KNOWN_MARKETPLACES_PATH, 'utf-8'));
|
|
318
310
|
if (marketplaces[MARKETPLACE_KEY]) {
|
|
319
311
|
delete marketplaces[MARKETPLACE_KEY];
|
|
320
|
-
fs.writeFileSync(
|
|
312
|
+
fs.writeFileSync(KNOWN_MARKETPLACES_PATH, JSON.stringify(marketplaces, null, 2));
|
|
321
313
|
console.log(`Removed marketplace "${MARKETPLACE_KEY}" from known_marketplaces.json`);
|
|
322
314
|
}
|
|
323
|
-
const marketplaceDir = path.join(
|
|
315
|
+
const marketplaceDir = path.join(CLAUDE_DIR, 'plugins', 'marketplaces', MARKETPLACE_KEY);
|
|
324
316
|
if (fs.existsSync(marketplaceDir)) {
|
|
325
317
|
fs.rmSync(marketplaceDir, { recursive: true, force: true });
|
|
326
318
|
console.log(`Removed marketplace directory: ${marketplaceDir}`);
|
|
@@ -332,41 +324,17 @@ try {
|
|
|
332
324
|
}
|
|
333
325
|
|
|
334
326
|
// Remove plugin cache
|
|
335
|
-
const pluginCacheDir = path.join(
|
|
336
|
-
let cacheRemoved = false;
|
|
327
|
+
const pluginCacheDir = path.join(CLAUDE_DIR, 'plugins', 'cache', 'bazilio-plugins', 'claude-notification-plugin');
|
|
337
328
|
if (fs.existsSync(pluginCacheDir)) {
|
|
338
329
|
fs.rmSync(pluginCacheDir, { recursive: true, force: true });
|
|
339
|
-
|
|
330
|
+
if (fs.existsSync(pluginCacheDir)) {
|
|
331
|
+
console.warn(`Warning: could not fully remove plugin cache directory:\n ${pluginCacheDir}\nPlease remove it manually.`);
|
|
332
|
+
} else {
|
|
333
|
+
console.log('Removed plugin cache');
|
|
334
|
+
}
|
|
340
335
|
}
|
|
341
336
|
|
|
342
|
-
|
|
343
|
-
? 'Hooks removed from settings.json'
|
|
344
|
-
: hooksRemoveError
|
|
345
|
-
? `Warning: ${hooksRemoveError}`
|
|
346
|
-
: 'No hooks found in settings.json';
|
|
347
|
-
|
|
348
|
-
const extras = [
|
|
349
|
-
listenerStopped ? 'Listener daemon stopped.' : '',
|
|
350
|
-
pluginEntryRemoved || cacheRemoved ? 'Plugin registration cleaned.' : '',
|
|
351
|
-
cliBinsRemoved ? 'CLI wrapper scripts removed.' : '',
|
|
352
|
-
].filter(Boolean).join('\n');
|
|
353
|
-
|
|
354
|
-
const cacheStillExists = fs.existsSync(pluginCacheDir);
|
|
355
|
-
if (cacheStillExists) {
|
|
356
|
-
console.log(`
|
|
357
|
-
Warning: Could not fully remove plugin cache directory:
|
|
358
|
-
${pluginCacheDir}
|
|
359
|
-
Please remove it manually.
|
|
360
|
-
${hooksStatus}
|
|
361
|
-
Config files deleted.${extras ? `\n${extras}` : ''}
|
|
362
|
-
`);
|
|
363
|
-
} else {
|
|
364
|
-
console.log(`
|
|
365
|
-
Claude Notification Plugin uninstalled.
|
|
366
|
-
${hooksStatus}
|
|
367
|
-
Config files deleted.${extras ? `\n${extras}` : ''}
|
|
368
|
-
`);
|
|
369
|
-
}
|
|
337
|
+
console.log('\nDone.\n');
|
|
370
338
|
|
|
371
339
|
// If run manually (not via npm lifecycle), remove the global npm package too
|
|
372
340
|
if (!process.env.npm_lifecycle_event) {
|
package/commit-sha
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
87470f0ddd7c4de3cdcbe91886dc3285b942c595
|
|
@@ -296,7 +296,7 @@ Re-run `claude-notify listener setup` anytime to reconfigure.
|
|
|
296
296
|
|
|
297
297
|
### Manual configuration
|
|
298
298
|
|
|
299
|
-
Full example of `~/.claude/
|
|
299
|
+
Full example of `~/.claude/claude-notify.config.json` with the listener section:
|
|
300
300
|
|
|
301
301
|
```json
|
|
302
302
|
{
|
|
@@ -882,7 +882,7 @@ claude-notify listener status
|
|
|
882
882
|
|
|
883
883
|
Check:
|
|
884
884
|
|
|
885
|
-
1. Does the config exist? `cat ~/.claude/
|
|
885
|
+
1. Does the config exist? `cat ~/.claude/claude-notify.config.json`
|
|
886
886
|
2. Are `telegram.token` and `telegram.chatId` present?
|
|
887
887
|
3. Is there a `listener.projects` section?
|
|
888
888
|
4. Logs: `claude-notify listener logs`
|
package/listener/listener.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// noinspection UnnecessaryLocalVariableJS
|
|
3
3
|
|
|
4
4
|
import fs from 'fs';
|
|
5
|
-
import os from 'os';
|
|
6
5
|
import path from 'path';
|
|
7
6
|
import process from 'process';
|
|
8
7
|
import { createLogger } from './logger.js';
|
|
@@ -12,13 +11,13 @@ import { WorkQueue } from './work-queue.js';
|
|
|
12
11
|
import { TaskRunner } from './task-runner.js';
|
|
13
12
|
import { WorktreeManager } from './worktree-manager.js';
|
|
14
13
|
import { parseMessage, parseTarget } from './message-parser.js';
|
|
14
|
+
import { CLAUDE_DIR, CONFIG_PATH, LISTENER_LOG_FILENAME } from '../bin/constants.js';
|
|
15
15
|
|
|
16
16
|
// ----------------------
|
|
17
17
|
// CONFIG
|
|
18
18
|
// ----------------------
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
const DEFAULT_LOG_DIR = path.join(os.homedir(), '.claude');
|
|
20
|
+
const DEFAULT_LOG_DIR = CLAUDE_DIR;
|
|
22
21
|
|
|
23
22
|
function loadConfig () {
|
|
24
23
|
try {
|
|
@@ -37,7 +36,7 @@ function loadConfig () {
|
|
|
37
36
|
const config = loadConfig();
|
|
38
37
|
const listenerLogDir = config.listener?.logDir || DEFAULT_LOG_DIR;
|
|
39
38
|
fs.mkdirSync(listenerLogDir, { recursive: true });
|
|
40
|
-
const logger = createLogger(path.join(listenerLogDir,
|
|
39
|
+
const logger = createLogger(path.join(listenerLogDir, LISTENER_LOG_FILENAME));
|
|
41
40
|
|
|
42
41
|
// Validate required fields
|
|
43
42
|
const token = process.env.CLAUDE_NOTIFY_TELEGRAM_TOKEN || config.telegramToken || config.telegram?.token;
|
package/notifier/notifier.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
import os from 'os';
|
|
5
4
|
import path from 'path';
|
|
6
5
|
import process from 'process';
|
|
7
6
|
import { execSync, spawn } from 'child_process';
|
|
7
|
+
import { CONFIG_PATH, STATE_PATH } from '../bin/constants.js';
|
|
8
8
|
|
|
9
9
|
// ----------------------
|
|
10
10
|
// CONFIG
|
|
@@ -40,7 +40,7 @@ function getBranch (cwd) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function loadConfig () {
|
|
43
|
-
const configPath =
|
|
43
|
+
const configPath = CONFIG_PATH;
|
|
44
44
|
|
|
45
45
|
const config = {
|
|
46
46
|
telegram: {
|
|
@@ -158,27 +158,19 @@ function isNotifierDisabled () {
|
|
|
158
158
|
return true;
|
|
159
159
|
}
|
|
160
160
|
// Skip notifications for listener-spawned tasks unless explicitly enabled
|
|
161
|
-
|
|
162
|
-
&& process.env.CLAUDE_NOTIFY_AFTER_LISTENER !== '1'
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
return false;
|
|
161
|
+
return process.env.CLAUDE_NOTIFY_FROM_LISTENER === '1'
|
|
162
|
+
&& process.env.CLAUDE_NOTIFY_AFTER_LISTENER !== '1';
|
|
163
|
+
|
|
166
164
|
}
|
|
167
165
|
|
|
168
166
|
// ----------------------
|
|
169
167
|
// STATE FILE
|
|
170
168
|
// ----------------------
|
|
171
169
|
|
|
172
|
-
const STATE_FILE = path.join(
|
|
173
|
-
os.homedir(),
|
|
174
|
-
'.claude',
|
|
175
|
-
'.notifier_state.json',
|
|
176
|
-
);
|
|
177
|
-
|
|
178
170
|
function loadState () {
|
|
179
|
-
if (fs.existsSync(
|
|
171
|
+
if (fs.existsSync(STATE_PATH)) {
|
|
180
172
|
try {
|
|
181
|
-
const raw = JSON.parse(fs.readFileSync(
|
|
173
|
+
const raw = JSON.parse(fs.readFileSync(STATE_PATH, 'utf-8'));
|
|
182
174
|
// Migrate flat state (pre-session format) to new format
|
|
183
175
|
if (!raw.sessions && raw.start !== undefined) {
|
|
184
176
|
return { sessions: {}, sentMessages: raw.sentMessages || [] };
|
|
@@ -198,9 +190,9 @@ function loadState () {
|
|
|
198
190
|
}
|
|
199
191
|
|
|
200
192
|
function saveState (state) {
|
|
201
|
-
const dir = path.dirname(
|
|
193
|
+
const dir = path.dirname(STATE_PATH);
|
|
202
194
|
fs.mkdirSync(dir, { recursive: true });
|
|
203
|
-
fs.writeFileSync(
|
|
195
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify(state));
|
|
204
196
|
}
|
|
205
197
|
|
|
206
198
|
function cleanStaleSessions (state) {
|
|
@@ -259,7 +251,7 @@ function markdownToTelegramHtml (md) {
|
|
|
259
251
|
result = result.replace(/~~(.+?)~~/g, '<s>$1</s>');
|
|
260
252
|
|
|
261
253
|
// Links: [text](url)
|
|
262
|
-
result = result.replace(/\[([^\]]+)
|
|
254
|
+
result = result.replace(/\[([^\]]+)]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
263
255
|
|
|
264
256
|
// Restore code blocks and inline codes
|
|
265
257
|
result = result.replace(/\x00CB(\d+)\x00/g, (_, i) => codeBlocks[i]);
|
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.10",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"node-notifier": "^10.0.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"eslint-plugin-import": "^2.31.0"
|
|
56
|
+
"eslint-plugin-import": "^2.31.0",
|
|
57
|
+
"eslint-plugin-unused-imports": "^4.4.1"
|
|
57
58
|
}
|
|
58
59
|
}
|