claude-notification-plugin 1.1.9 → 1.1.12
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 +4 -3
- package/bin/constants.js +33 -0
- package/bin/install.js +35 -48
- package/bin/listener-cli.js +22 -26
- package/bin/uninstall.js +25 -35
- package/commit-sha +1 -1
- package/listener/LISTENER-DETAILED.md +2 -2
- package/listener/listener.js +3 -4
- package/notifier/notifier.js +24 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,11 +48,12 @@ claude-notify install
|
|
|
48
48
|
claude-notify uninstall
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
This removes hooks,
|
|
51
|
+
This removes hooks, CLI wrappers, plugin registration, and the npm global package.
|
|
52
|
+
Your config file (`~/.claude/claude-notify.config.json`) is preserved so settings survive reinstalls.
|
|
52
53
|
|
|
53
54
|
## Configuration
|
|
54
55
|
|
|
55
|
-
Config file: `~/.claude/
|
|
56
|
+
Config file: `~/.claude/claude-notify.config.json`
|
|
56
57
|
|
|
57
58
|
```json
|
|
58
59
|
{
|
|
@@ -293,7 +294,7 @@ Alternative: add **@userinfobot** to a chat and it will reply with the ID.
|
|
|
293
294
|
|
|
294
295
|
```
|
|
295
296
|
claude-notify install Reinstall plugin registration, Telegram config, hooks
|
|
296
|
-
claude-notify uninstall Remove plugin, hooks,
|
|
297
|
+
claude-notify uninstall Remove plugin, hooks, CLI wrappers (config preserved)
|
|
297
298
|
claude-notify listener <action> Manage the Telegram Listener daemon
|
|
298
299
|
Actions: start, stop, status, setup, logs, restart
|
|
299
300
|
```
|
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',
|
|
@@ -451,9 +438,9 @@ async function main () {
|
|
|
451
438
|
|
|
452
439
|
// 2. Interactive Telegram setup
|
|
453
440
|
let existing = {};
|
|
454
|
-
if (fs.existsSync(
|
|
441
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
455
442
|
try {
|
|
456
|
-
existing = JSON.parse(fs.readFileSync(
|
|
443
|
+
existing = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
457
444
|
} catch {
|
|
458
445
|
// ignore malformed config
|
|
459
446
|
}
|
|
@@ -522,7 +509,7 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
522
509
|
}
|
|
523
510
|
|
|
524
511
|
// 3. Write config
|
|
525
|
-
fs.mkdirSync(
|
|
512
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
526
513
|
|
|
527
514
|
const platform = process.platform;
|
|
528
515
|
let defaultSoundFile;
|
|
@@ -556,7 +543,7 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
556
543
|
debug: false,
|
|
557
544
|
listener: {
|
|
558
545
|
projects: {},
|
|
559
|
-
worktreeBaseDir: path.join(
|
|
546
|
+
worktreeBaseDir: path.join(HOME, '.claude', 'worktrees'),
|
|
560
547
|
autoCreateWorktree: true,
|
|
561
548
|
taskTimeoutMinutes: 30,
|
|
562
549
|
maxQueuePerWorkDir: 10,
|
|
@@ -580,15 +567,15 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
580
567
|
config.telegram.chatId = chatId;
|
|
581
568
|
}
|
|
582
569
|
|
|
583
|
-
fs.writeFileSync(
|
|
584
|
-
console.log(` Config saved: ${
|
|
570
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
571
|
+
console.log(` Config saved: ${CONFIG_PATH}`);
|
|
585
572
|
|
|
586
573
|
// 4. Register hooks
|
|
587
574
|
let settings = {};
|
|
588
575
|
|
|
589
|
-
if (fs.existsSync(
|
|
576
|
+
if (fs.existsSync(SETTINGS_PATH)) {
|
|
590
577
|
try {
|
|
591
|
-
settings = JSON.parse(fs.readFileSync(
|
|
578
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
592
579
|
} catch {
|
|
593
580
|
settings = {};
|
|
594
581
|
}
|
|
@@ -617,8 +604,8 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
617
604
|
};
|
|
618
605
|
console.log(' Registered marketplace in extraKnownMarketplaces');
|
|
619
606
|
|
|
620
|
-
fs.writeFileSync(
|
|
621
|
-
console.log(` Settings saved: ${
|
|
607
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
608
|
+
console.log(` Settings saved: ${SETTINGS_PATH}`);
|
|
622
609
|
|
|
623
610
|
// 5. Summary
|
|
624
611
|
const telegramStatus = config.telegram.token && config.telegram.chatId
|
|
@@ -642,10 +629,10 @@ Plugin hooks (via hooks/hooks.json):
|
|
|
642
629
|
- Stop (task finished)
|
|
643
630
|
- Notification (waiting for input)
|
|
644
631
|
|
|
645
|
-
Config: ${
|
|
632
|
+
Config: ${CONFIG_PATH}
|
|
646
633
|
${telegramStatus}${platformTip}
|
|
647
634
|
|
|
648
|
-
Log: ${
|
|
635
|
+
Log: ${INSTALL_LOG_PATH}
|
|
649
636
|
|
|
650
637
|
To uninstall: claude-notify uninstall
|
|
651
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
|
+
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,7 +166,7 @@ function stopListenerIfRunning () {
|
|
|
172
166
|
}
|
|
173
167
|
}
|
|
174
168
|
|
|
175
|
-
removeFileIfExists(
|
|
169
|
+
removeFileIfExists(PID_PATH);
|
|
176
170
|
return stopped;
|
|
177
171
|
}
|
|
178
172
|
|
|
@@ -182,9 +176,9 @@ console.log('\nUninstalling Claude Notification Plugin...\n');
|
|
|
182
176
|
stopListenerIfRunning();
|
|
183
177
|
|
|
184
178
|
// Remove hooks from settings.json
|
|
185
|
-
if (fs.existsSync(
|
|
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) {
|
|
@@ -227,10 +221,10 @@ if (fs.existsSync(settingsPath)) {
|
|
|
227
221
|
console.log('Removed marketplace from extraKnownMarketplaces in settings.json');
|
|
228
222
|
}
|
|
229
223
|
|
|
230
|
-
fs.writeFileSync(
|
|
224
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
231
225
|
|
|
232
226
|
// Verify hooks were actually removed
|
|
233
|
-
const verify = JSON.parse(fs.readFileSync(
|
|
227
|
+
const verify = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
234
228
|
const remainingPluginHooks = verify.hooks
|
|
235
229
|
? Object.values(verify.hooks).some((matchers) =>
|
|
236
230
|
Array.isArray(matchers) &&
|
|
@@ -247,10 +241,8 @@ if (fs.existsSync(settingsPath)) {
|
|
|
247
241
|
}
|
|
248
242
|
}
|
|
249
243
|
|
|
250
|
-
// Remove
|
|
251
|
-
const
|
|
252
|
-
const listenerLogFile = path.join(claudeDir, '.cc-n-listener.log');
|
|
253
|
-
for (const file of [configPath, statePath, resolverPath, pidFile, listenerLogFile]) {
|
|
244
|
+
// Remove state, resolver, and listener files (config is preserved for reinstall)
|
|
245
|
+
for (const file of [STATE_PATH, RESOLVER_PATH, PID_PATH, LISTENER_LOG_PATH]) {
|
|
254
246
|
if (fs.existsSync(file)) {
|
|
255
247
|
fs.unlinkSync(file);
|
|
256
248
|
console.log(`Removed ${path.basename(file)}`);
|
|
@@ -274,7 +266,7 @@ try {
|
|
|
274
266
|
}
|
|
275
267
|
|
|
276
268
|
// ~/.local/bin (common on Linux/macOS, also used on Windows)
|
|
277
|
-
wrapperDirs.add(path.join(
|
|
269
|
+
wrapperDirs.add(path.join(HOME, '.local', 'bin'));
|
|
278
270
|
|
|
279
271
|
for (const dir of wrapperDirs) {
|
|
280
272
|
for (const name of WRAPPER_NAMES) {
|
|
@@ -287,13 +279,12 @@ for (const dir of wrapperDirs) {
|
|
|
287
279
|
}
|
|
288
280
|
|
|
289
281
|
// Remove from installed_plugins.json
|
|
290
|
-
|
|
291
|
-
if (fs.existsSync(installedPluginsPath)) {
|
|
282
|
+
if (fs.existsSync(INSTALLED_PLUGINS_PATH)) {
|
|
292
283
|
try {
|
|
293
|
-
const data = JSON.parse(fs.readFileSync(
|
|
284
|
+
const data = JSON.parse(fs.readFileSync(INSTALLED_PLUGINS_PATH, 'utf-8'));
|
|
294
285
|
if (data.plugins?.[PLUGIN_KEY]) {
|
|
295
286
|
delete data.plugins[PLUGIN_KEY];
|
|
296
|
-
fs.writeFileSync(
|
|
287
|
+
fs.writeFileSync(INSTALLED_PLUGINS_PATH, JSON.stringify(data, null, 2));
|
|
297
288
|
console.log('Removed plugin from installed_plugins.json');
|
|
298
289
|
}
|
|
299
290
|
} catch {
|
|
@@ -302,13 +293,12 @@ if (fs.existsSync(installedPluginsPath)) {
|
|
|
302
293
|
}
|
|
303
294
|
|
|
304
295
|
// Remove marketplace from known_marketplaces.json if no plugins reference it
|
|
305
|
-
const knownMarketplacesPath = path.join(claudeDir, 'plugins', 'known_marketplaces.json');
|
|
306
296
|
try {
|
|
307
|
-
if (fs.existsSync(
|
|
297
|
+
if (fs.existsSync(KNOWN_MARKETPLACES_PATH)) {
|
|
308
298
|
// Check if any remaining plugins reference this marketplace
|
|
309
299
|
let hasMarketplacePlugins = false;
|
|
310
|
-
if (fs.existsSync(
|
|
311
|
-
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'));
|
|
312
302
|
if (data.plugins) {
|
|
313
303
|
hasMarketplacePlugins = Object.keys(data.plugins)
|
|
314
304
|
.some((key) => key.endsWith(`@${MARKETPLACE_KEY}`));
|
|
@@ -316,13 +306,13 @@ try {
|
|
|
316
306
|
}
|
|
317
307
|
|
|
318
308
|
if (!hasMarketplacePlugins) {
|
|
319
|
-
const marketplaces = JSON.parse(fs.readFileSync(
|
|
309
|
+
const marketplaces = JSON.parse(fs.readFileSync(KNOWN_MARKETPLACES_PATH, 'utf-8'));
|
|
320
310
|
if (marketplaces[MARKETPLACE_KEY]) {
|
|
321
311
|
delete marketplaces[MARKETPLACE_KEY];
|
|
322
|
-
fs.writeFileSync(
|
|
312
|
+
fs.writeFileSync(KNOWN_MARKETPLACES_PATH, JSON.stringify(marketplaces, null, 2));
|
|
323
313
|
console.log(`Removed marketplace "${MARKETPLACE_KEY}" from known_marketplaces.json`);
|
|
324
314
|
}
|
|
325
|
-
const marketplaceDir = path.join(
|
|
315
|
+
const marketplaceDir = path.join(CLAUDE_DIR, 'plugins', 'marketplaces', MARKETPLACE_KEY);
|
|
326
316
|
if (fs.existsSync(marketplaceDir)) {
|
|
327
317
|
fs.rmSync(marketplaceDir, { recursive: true, force: true });
|
|
328
318
|
console.log(`Removed marketplace directory: ${marketplaceDir}`);
|
|
@@ -334,7 +324,7 @@ try {
|
|
|
334
324
|
}
|
|
335
325
|
|
|
336
326
|
// Remove plugin cache
|
|
337
|
-
const pluginCacheDir = path.join(
|
|
327
|
+
const pluginCacheDir = path.join(CLAUDE_DIR, 'plugins', 'cache', 'bazilio-plugins', 'claude-notification-plugin');
|
|
338
328
|
if (fs.existsSync(pluginCacheDir)) {
|
|
339
329
|
fs.rmSync(pluginCacheDir, { recursive: true, force: true });
|
|
340
330
|
if (fs.existsSync(pluginCacheDir)) {
|
package/commit-sha
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
5b2891cb5b43d7ccdd81b8d6541498b52ef549ee
|
|
@@ -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]);
|
|
@@ -398,9 +390,12 @@ async function sendDesktopNotification (config, message) {
|
|
|
398
390
|
}
|
|
399
391
|
try {
|
|
400
392
|
const { default: notifier } = await import('node-notifier');
|
|
393
|
+
const iconPath = new URL('../claude_img/claude.png', import.meta.url).pathname
|
|
394
|
+
.replace(/^\/([a-zA-Z]:)/, '$1');
|
|
401
395
|
notifier.notify({
|
|
402
396
|
title: 'Claude Code',
|
|
403
397
|
message,
|
|
398
|
+
icon: iconPath,
|
|
404
399
|
sound: false,
|
|
405
400
|
wait: false,
|
|
406
401
|
});
|
|
@@ -684,21 +679,21 @@ process.stdin.on('end', async () => {
|
|
|
684
679
|
process.exit(0);
|
|
685
680
|
}
|
|
686
681
|
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
: 'Claude finished coding';
|
|
682
|
+
const statusEmoji = eventType === 'Notification' ? '⏸' : '✅';
|
|
683
|
+
const desktopStatus = eventType === 'Notification' ? 'Waiting' : 'Finished';
|
|
690
684
|
|
|
691
685
|
const branch = getBranch(cwd);
|
|
692
|
-
const
|
|
693
|
-
const
|
|
686
|
+
const label = branch ? `@${project}/${branch}` : `@${project}`;
|
|
687
|
+
const labelHtml = branch
|
|
688
|
+
? `@<b>${escapeHtml(project)}</b>/<b>${escapeHtml(branch)}</b>`
|
|
689
|
+
: `@<b>${escapeHtml(project)}</b>`;
|
|
694
690
|
|
|
695
691
|
const triggerLine = config.debug ? `\nTrigger: ${eventType}` : '';
|
|
696
692
|
|
|
697
|
-
|
|
698
|
-
`${title}\n\nProject: ${project}${branchLine}\nDuration: ${duration}s${triggerLine}`;
|
|
693
|
+
const desktopMessage = `${desktopStatus}: ${label}`;
|
|
699
694
|
|
|
700
695
|
let telegramMessage =
|
|
701
|
-
`${
|
|
696
|
+
`${statusEmoji} ${labelHtml} (duration: ${duration}s)${triggerLine}`;
|
|
702
697
|
|
|
703
698
|
if (config.telegram.includeLastCcMessageInTelegram && event.last_assistant_message) {
|
|
704
699
|
const maxLen = 3500;
|
|
@@ -710,18 +705,14 @@ process.stdin.on('end', async () => {
|
|
|
710
705
|
}
|
|
711
706
|
|
|
712
707
|
if (config.debug) {
|
|
713
|
-
const debugBlockMd = '\n\n*Debug:*\n'
|
|
714
|
-
+ (config.voice.enabled ? `\nVoice: ${getVoicePhrase(duration)}` : '')
|
|
715
|
-
+ `\n\nHook input:\n\`\`\`json\n${JSON.stringify(event, null, 2)}\n\`\`\``;
|
|
716
708
|
const debugBlockHtml = '\n\n<b>Debug:</b>\n'
|
|
717
709
|
+ (config.voice.enabled ? `\nVoice: ${escapeHtml(getVoicePhrase(duration))}` : '')
|
|
718
710
|
+ `\n\nHook input:\n<pre>${escapeHtml(JSON.stringify(event, null, 2))}</pre>`;
|
|
719
|
-
message += debugBlockMd;
|
|
720
711
|
telegramMessage += debugBlockHtml;
|
|
721
712
|
}
|
|
722
713
|
|
|
723
714
|
await sendWebhook(config, {
|
|
724
|
-
title
|
|
715
|
+
title: `${desktopStatus}: ${label}`,
|
|
725
716
|
project,
|
|
726
717
|
branch: branch || undefined,
|
|
727
718
|
duration,
|
|
@@ -730,7 +721,7 @@ process.stdin.on('end', async () => {
|
|
|
730
721
|
hookEvent: event,
|
|
731
722
|
});
|
|
732
723
|
|
|
733
|
-
state._telegramText =
|
|
724
|
+
state._telegramText = telegramMessage;
|
|
734
725
|
await sendTelegram(config, state);
|
|
735
726
|
delete state._telegramText;
|
|
736
727
|
if (eventType === 'Stop') {
|
|
@@ -738,7 +729,7 @@ process.stdin.on('end', async () => {
|
|
|
738
729
|
}
|
|
739
730
|
saveState(state);
|
|
740
731
|
|
|
741
|
-
await sendDesktopNotification(config,
|
|
732
|
+
await sendDesktopNotification(config, desktopMessage);
|
|
742
733
|
playSound(config);
|
|
743
734
|
speakResult(config, duration);
|
|
744
735
|
});
|
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.12",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|