claude-notification-plugin 1.0.59 → 1.0.65

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.
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import { spawn, execSync } from 'child_process';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const PID_FILE = path.join(os.homedir(), '.claude', '.listener.pid');
13
+ const LOG_FILE = path.join(os.homedir(), '.claude', '.listener.log');
14
+ const CONFIG_FILE = path.join(os.homedir(), '.claude', 'notifier.config.json');
15
+ const LISTENER_SCRIPT = path.join(__dirname, '..', 'listener', 'listener.js');
16
+
17
+ const command = process.argv[2];
18
+
19
+ switch (command) {
20
+ case 'start':
21
+ startDaemon();
22
+ break;
23
+ case 'stop':
24
+ stopDaemon();
25
+ break;
26
+ case 'status':
27
+ showStatus();
28
+ break;
29
+ case 'logs':
30
+ showLogs();
31
+ break;
32
+ case 'restart':
33
+ stopDaemon();
34
+ setTimeout(() => startDaemon(), 1000);
35
+ break;
36
+ default:
37
+ console.log('Usage: claude-notify-listener <start|stop|status|logs|restart>');
38
+ console.log('');
39
+ console.log('Commands:');
40
+ console.log(' start Start the listener daemon');
41
+ console.log(' stop Stop the listener daemon');
42
+ console.log(' status Show daemon status');
43
+ console.log(' logs Show recent log entries');
44
+ console.log(' restart Restart the daemon');
45
+ process.exit(command ? 1 : 0);
46
+ }
47
+
48
+ function startDaemon () {
49
+ // Check if already running
50
+ const existingPid = readPid();
51
+ if (existingPid && isProcessAlive(existingPid)) {
52
+ console.log(`Listener is already running (PID: ${existingPid})`);
53
+ process.exit(1);
54
+ }
55
+
56
+ // Clean stale PID file
57
+ if (existingPid) {
58
+ try {
59
+ fs.unlinkSync(PID_FILE);
60
+ } catch {
61
+ // ignore
62
+ }
63
+ }
64
+
65
+ // Validate config
66
+ if (!fs.existsSync(CONFIG_FILE)) {
67
+ console.error(`Config not found: ${CONFIG_FILE}`);
68
+ console.error('Run claude-notify-install first, or create the config manually.');
69
+ process.exit(1);
70
+ }
71
+
72
+ let config;
73
+ try {
74
+ config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
75
+ } catch (err) {
76
+ console.error(`Invalid config: ${err.message}`);
77
+ process.exit(1);
78
+ }
79
+
80
+ const token = process.env.CLAUDE_NOTIFY_TELEGRAM_TOKEN || config.telegramToken || config.telegram?.token;
81
+ const chatId = process.env.CLAUDE_NOTIFY_TELEGRAM_CHAT_ID || config.telegramChatId || config.telegram?.chatId;
82
+
83
+ if (!token || !chatId) {
84
+ console.error('Missing telegramToken or telegramChatId in config.');
85
+ console.error('These are required for the listener to receive messages.');
86
+ process.exit(1);
87
+ }
88
+
89
+ if (!config.listener?.projects || Object.keys(config.listener.projects).length === 0) {
90
+ console.error('No projects defined in config.listener.projects');
91
+ console.error('');
92
+ console.error('Add projects to your config:');
93
+ console.error(JSON.stringify({
94
+ listener: {
95
+ projects: {
96
+ default: { path: '/path/to/your/project' },
97
+ },
98
+ },
99
+ }, null, 2));
100
+ process.exit(1);
101
+ }
102
+
103
+ // Ensure log directory exists
104
+ fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
105
+
106
+ // Open log file for child stdio
107
+ const logFd = fs.openSync(LOG_FILE, 'a');
108
+
109
+ // Spawn detached child
110
+ const child = spawn(process.execPath, [LISTENER_SCRIPT], {
111
+ detached: true,
112
+ stdio: ['ignore', logFd, logFd],
113
+ env: { ...process.env },
114
+ windowsHide: true,
115
+ });
116
+
117
+ child.unref();
118
+ fs.closeSync(logFd);
119
+
120
+ // Write PID
121
+ fs.mkdirSync(path.dirname(PID_FILE), { recursive: true });
122
+ fs.writeFileSync(PID_FILE, String(child.pid));
123
+
124
+ console.log(`Listener started (PID: ${child.pid})`);
125
+ console.log(`Log: ${LOG_FILE}`);
126
+ console.log(`Projects: ${Object.keys(config.listener.projects).join(', ')}`);
127
+ }
128
+
129
+ function stopDaemon () {
130
+ const pid = readPid();
131
+ if (!pid) {
132
+ console.log('Listener is not running (no PID file)');
133
+ return;
134
+ }
135
+
136
+ if (!isProcessAlive(pid)) {
137
+ console.log(`Listener is not running (stale PID: ${pid})`);
138
+ cleanPid();
139
+ return;
140
+ }
141
+
142
+ console.log(`Stopping listener (PID: ${pid})...`);
143
+
144
+ try {
145
+ if (process.platform === 'win32') {
146
+ execSync(`taskkill /PID ${pid} /T /F`, {
147
+ stdio: 'ignore',
148
+ windowsHide: true,
149
+ });
150
+ } else {
151
+ process.kill(pid, 'SIGTERM');
152
+ // Wait for graceful shutdown
153
+ let tries = 10;
154
+ while (tries-- > 0 && isProcessAlive(pid)) {
155
+ execSync('sleep 0.5', { stdio: 'ignore' });
156
+ }
157
+ if (isProcessAlive(pid)) {
158
+ process.kill(pid, 'SIGKILL');
159
+ }
160
+ }
161
+ } catch {
162
+ // Process may already be dead
163
+ }
164
+
165
+ cleanPid();
166
+ console.log('Listener stopped');
167
+ }
168
+
169
+ function showStatus () {
170
+ const pid = readPid();
171
+ if (!pid) {
172
+ console.log('Status: not running');
173
+ return;
174
+ }
175
+
176
+ if (!isProcessAlive(pid)) {
177
+ console.log(`Status: not running (stale PID: ${pid})`);
178
+ cleanPid();
179
+ return;
180
+ }
181
+
182
+ console.log(`Status: running (PID: ${pid})`);
183
+ console.log(`Log: ${LOG_FILE}`);
184
+
185
+ // Show last few log lines
186
+ try {
187
+ if (fs.existsSync(LOG_FILE)) {
188
+ const content = fs.readFileSync(LOG_FILE, 'utf-8');
189
+ const lines = content.trim().split('\n');
190
+ const last = lines.slice(-5);
191
+ console.log('\nRecent log:');
192
+ for (const line of last) {
193
+ console.log(` ${line}`);
194
+ }
195
+ }
196
+ } catch {
197
+ // ignore
198
+ }
199
+ }
200
+
201
+ function showLogs () {
202
+ try {
203
+ if (!fs.existsSync(LOG_FILE)) {
204
+ console.log('No log file found');
205
+ return;
206
+ }
207
+ const content = fs.readFileSync(LOG_FILE, 'utf-8');
208
+ const lines = content.trim().split('\n');
209
+ const last = lines.slice(-50);
210
+ for (const line of last) {
211
+ console.log(line);
212
+ }
213
+ } catch (err) {
214
+ console.error(`Failed to read log: ${err.message}`);
215
+ }
216
+ }
217
+
218
+ function readPid () {
219
+ try {
220
+ if (fs.existsSync(PID_FILE)) {
221
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10);
222
+ return isNaN(pid) ? null : pid;
223
+ }
224
+ } catch {
225
+ // ignore
226
+ }
227
+ return null;
228
+ }
229
+
230
+ function cleanPid () {
231
+ try {
232
+ if (fs.existsSync(PID_FILE)) {
233
+ fs.unlinkSync(PID_FILE);
234
+ }
235
+ } catch {
236
+ // ignore
237
+ }
238
+ }
239
+
240
+ function isProcessAlive (pid) {
241
+ try {
242
+ if (process.platform === 'win32') {
243
+ const result = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
244
+ encoding: 'utf-8',
245
+ windowsHide: true,
246
+ stdio: ['pipe', 'pipe', 'ignore'],
247
+ });
248
+ return result.includes(String(pid));
249
+ }
250
+ process.kill(pid, 0);
251
+ return true;
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
@@ -0,0 +1,100 @@
1
+ ---
2
+ description: Manage the Telegram Listener daemon (start, stop, status, logs, restart)
3
+ ---
4
+
5
+ # Listener Management
6
+
7
+ Manage the Telegram Listener daemon that receives tasks from Telegram and executes them via `claude -p`.
8
+
9
+ The user invokes this command as `/claude-notification-plugin:listener <action>`.
10
+
11
+ ## Actions
12
+
13
+ Determine the action from the user's input. If no action is provided, show the help text below and ask which action they want.
14
+
15
+ ### start
16
+
17
+ Start the listener daemon.
18
+
19
+ 1. Determine the plugin root path — use the directory where this command file lives: `${CLAUDE_PLUGIN_ROOT}`. The listener CLI script is at `${CLAUDE_PLUGIN_ROOT}/bin/listener-cli.js`.
20
+ 2. Run in a shell:
21
+ ```bash
22
+ node "${CLAUDE_PLUGIN_ROOT}/bin/listener-cli.js" start
23
+ ```
24
+ 3. Show the output to the user. If it says "Listener started", confirm success. If there is an error (missing config, missing projects, already running), explain what to do.
25
+
26
+ ### stop
27
+
28
+ Stop the listener daemon.
29
+
30
+ 1. Run:
31
+ ```bash
32
+ node "${CLAUDE_PLUGIN_ROOT}/bin/listener-cli.js" stop
33
+ ```
34
+ 2. Show the output.
35
+
36
+ ### status
37
+
38
+ Show the listener status.
39
+
40
+ 1. Run:
41
+ ```bash
42
+ node "${CLAUDE_PLUGIN_ROOT}/bin/listener-cli.js" status
43
+ ```
44
+ 2. Show the output.
45
+
46
+ ### logs
47
+
48
+ Show recent log entries.
49
+
50
+ 1. Run:
51
+ ```bash
52
+ node "${CLAUDE_PLUGIN_ROOT}/bin/listener-cli.js" logs
53
+ ```
54
+ 2. Show the output.
55
+
56
+ ### restart
57
+
58
+ Restart the listener daemon.
59
+
60
+ 1. Run:
61
+ ```bash
62
+ node "${CLAUDE_PLUGIN_ROOT}/bin/listener-cli.js" restart
63
+ ```
64
+ 2. Show the output.
65
+
66
+ ## Help text
67
+
68
+ If the user doesn't specify an action, show:
69
+
70
+ ```
71
+ Telegram Listener — receives tasks from Telegram and executes via claude -p.
72
+
73
+ Usage:
74
+ /claude-notification-plugin:listener start — Start the daemon
75
+ /claude-notification-plugin:listener stop — Stop the daemon
76
+ /claude-notification-plugin:listener status — Show status
77
+ /claude-notification-plugin:listener logs — Show recent logs
78
+ /claude-notification-plugin:listener restart — Restart the daemon
79
+
80
+ Prerequisites:
81
+ 1. Telegram bot configured (token + chatId in ~/.claude/notifier.config.json)
82
+ 2. "listener.projects" section in config with at least one project
83
+
84
+ See LISTENER.md for detailed documentation.
85
+ ```
86
+
87
+ ## Important
88
+
89
+ - The `${CLAUDE_PLUGIN_ROOT}` variable resolves to the plugin installation directory. Always use it to build the path to `bin/listener-cli.js`.
90
+ - If the config doesn't have a `listener.projects` section, tell the user to add one and show a minimal example:
91
+ ```json
92
+ {
93
+ "listener": {
94
+ "projects": {
95
+ "default": { "path": "/path/to/your/project" }
96
+ }
97
+ }
98
+ }
99
+ ```
100
+ - If Telegram credentials are missing, suggest running `/claude-notification-plugin:setup` first.