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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +261 -208
- package/bin/listener-cli.js +255 -0
- package/commands/listener.md +100 -0
- package/listener/listener.js +613 -0
- package/listener/logger.js +46 -0
- package/listener/message-parser.js +100 -0
- package/listener/task-runner.js +148 -0
- package/listener/telegram-poller.js +142 -0
- package/listener/work-queue.js +306 -0
- package/listener/worktree-manager.js +279 -0
- package/notifier/notifier.js +4 -2
- package/package.json +4 -2
|
@@ -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.
|