claude-notification-plugin 1.0.76 → 1.0.80
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 +22 -0
- package/bin/install.js +10 -0
- package/bin/listener-cli.js +22 -9
- package/bin/register-cli.js +142 -0
- package/bin/uninstall.js +24 -0
- package/commands/setup.md +9 -1
- package/listener/LISTENER-DETAILED.md +48 -4
- package/listener/listener.js +9 -3
- package/listener/task-logger.js +68 -0
- package/listener/task-runner.js +12 -1
- package/listener/work-queue.js +2 -0
- package/notifier/notifier.js +10 -2
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.80",
|
|
4
4
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Viacheslav Makarov",
|
package/README.md
CHANGED
|
@@ -165,6 +165,10 @@ ENV: **CLAUDE_NOTIFY_DISABLE**
|
|
|
165
165
|
Set to `1` to disable all notifications for the current project.
|
|
166
166
|
Default: **0**
|
|
167
167
|
|
|
168
|
+
ENV: **CLAUDE_NOTIFY_AFTER_LISTENER**
|
|
169
|
+
When a task is started by the Telegram Listener, notifications are suppressed by default to avoid duplicates (the listener sends its own status messages). Set to `1` to enable notifier notifications for listener-spawned tasks.
|
|
170
|
+
Default: **0**
|
|
171
|
+
|
|
168
172
|
### Per-project configuration
|
|
169
173
|
|
|
170
174
|
Add to `.claude/settings.local.json` in the project root to control channels per project:
|
|
@@ -235,6 +239,24 @@ Alternative for Chat ID: add **@userinfobot** to a chat and it will reply with t
|
|
|
235
239
|
|
|
236
240
|
Your remote control for Claude: send a message in Telegram, and the task starts running on your PC. See **[LISTENER.md](LISTENER.md)** for the full guide.
|
|
237
241
|
|
|
242
|
+
When installed as a plugin, you can manage the listener daemon directly from Claude Code with the `/listener` slash command:
|
|
243
|
+
`/claude-notification-plugin:listener start | stop | status | logs | restart`
|
|
244
|
+
|
|
245
|
+
## CLI Commands Registration
|
|
246
|
+
|
|
247
|
+
When installed as a **plugin**, CLI commands (`claude-notify-listener`, etc.) are not in PATH.
|
|
248
|
+
The `/claude-notification-plugin:setup` command registers them automatically by placing wrapper scripts next to the `claude` binary.
|
|
249
|
+
|
|
250
|
+
To manage manually:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
claude-notify-register # register CLI commands
|
|
254
|
+
claude-notify-register list # show registration status
|
|
255
|
+
claude-notify-register unregister # remove CLI commands
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
When installed via `npm install -g`, npm links the commands itself — no extra step needed.
|
|
259
|
+
|
|
238
260
|
## Testing (load without install)
|
|
239
261
|
|
|
240
262
|
```bash
|
package/bin/install.js
CHANGED
|
@@ -166,6 +166,16 @@ async function main () {
|
|
|
166
166
|
notifyAfterSeconds: 15,
|
|
167
167
|
notifyOnWaiting: false,
|
|
168
168
|
debug: false,
|
|
169
|
+
listener: {
|
|
170
|
+
projects: {},
|
|
171
|
+
worktreeBaseDir: path.join(home, '.claude', 'worktrees'),
|
|
172
|
+
autoCreateWorktree: true,
|
|
173
|
+
taskTimeoutMinutes: 30,
|
|
174
|
+
maxQueuePerWorkDir: 10,
|
|
175
|
+
maxTotalTasks: 50,
|
|
176
|
+
logDir: '',
|
|
177
|
+
taskLogDir: '',
|
|
178
|
+
},
|
|
169
179
|
};
|
|
170
180
|
|
|
171
181
|
// Merge defaults with existing config (user values take priority)
|
package/bin/listener-cli.js
CHANGED
|
@@ -9,9 +9,19 @@ import { fileURLToPath } from 'url';
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
11
11
|
|
|
12
|
+
const DEFAULT_LOG_DIR = path.join(os.homedir(), '.claude');
|
|
12
13
|
const PID_FILE = path.join(os.homedir(), '.claude', '.listener.pid');
|
|
13
|
-
const LOG_FILE = path.join(os.homedir(), '.claude', '.listener.log');
|
|
14
14
|
const CONFIG_FILE = path.join(os.homedir(), '.claude', 'notifier.config.json');
|
|
15
|
+
|
|
16
|
+
function getLogFile () {
|
|
17
|
+
try {
|
|
18
|
+
const cfg = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
19
|
+
const logDir = cfg.listener?.logDir || DEFAULT_LOG_DIR;
|
|
20
|
+
return path.join(logDir, '.cc-n-listener.log');
|
|
21
|
+
} catch {
|
|
22
|
+
return path.join(DEFAULT_LOG_DIR, '.cc-n-listener.log');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
15
25
|
const LISTENER_SCRIPT = path.join(__dirname, '..', 'listener', 'listener.js');
|
|
16
26
|
|
|
17
27
|
const command = process.argv[2];
|
|
@@ -101,10 +111,11 @@ function startDaemon () {
|
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
// Ensure log directory exists
|
|
104
|
-
|
|
114
|
+
const logFile = getLogFile();
|
|
115
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
105
116
|
|
|
106
117
|
// Open log file for child stdio
|
|
107
|
-
const logFd = fs.openSync(
|
|
118
|
+
const logFd = fs.openSync(logFile, 'a');
|
|
108
119
|
|
|
109
120
|
// Spawn detached child
|
|
110
121
|
const child = spawn(process.execPath, [LISTENER_SCRIPT], {
|
|
@@ -122,7 +133,7 @@ function startDaemon () {
|
|
|
122
133
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
123
134
|
|
|
124
135
|
console.log(`Listener started (PID: ${child.pid})`);
|
|
125
|
-
console.log(`Log: ${
|
|
136
|
+
console.log(`Log: ${logFile}`);
|
|
126
137
|
console.log(`Projects: ${Object.keys(config.listener.projects).join(', ')}`);
|
|
127
138
|
}
|
|
128
139
|
|
|
@@ -179,13 +190,14 @@ function showStatus () {
|
|
|
179
190
|
return;
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
const logFile = getLogFile();
|
|
182
194
|
console.log(`Status: running (PID: ${pid})`);
|
|
183
|
-
console.log(`Log: ${
|
|
195
|
+
console.log(`Log: ${logFile}`);
|
|
184
196
|
|
|
185
197
|
// Show last few log lines
|
|
186
198
|
try {
|
|
187
|
-
if (fs.existsSync(
|
|
188
|
-
const content = fs.readFileSync(
|
|
199
|
+
if (fs.existsSync(logFile)) {
|
|
200
|
+
const content = fs.readFileSync(logFile, 'utf-8');
|
|
189
201
|
const lines = content.trim().split('\n');
|
|
190
202
|
const last = lines.slice(-5);
|
|
191
203
|
console.log('\nRecent log:');
|
|
@@ -199,12 +211,13 @@ function showStatus () {
|
|
|
199
211
|
}
|
|
200
212
|
|
|
201
213
|
function showLogs () {
|
|
214
|
+
const logFile = getLogFile();
|
|
202
215
|
try {
|
|
203
|
-
if (!fs.existsSync(
|
|
216
|
+
if (!fs.existsSync(logFile)) {
|
|
204
217
|
console.log('No log file found');
|
|
205
218
|
return;
|
|
206
219
|
}
|
|
207
|
-
const content = fs.readFileSync(
|
|
220
|
+
const content = fs.readFileSync(logFile, 'utf-8');
|
|
208
221
|
const lines = content.trim().split('\n');
|
|
209
222
|
const last = lines.slice(-50);
|
|
210
223
|
for (const line of last) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
const PLUGIN_ROOT = path.resolve(__dirname, '..');
|
|
12
|
+
|
|
13
|
+
const BINS = {
|
|
14
|
+
'claude-notify-listener': 'bin/listener-cli.js',
|
|
15
|
+
'claude-notify-install': 'bin/install.js',
|
|
16
|
+
'claude-notify-uninstall': 'bin/uninstall.js',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function findClaudeDir () {
|
|
20
|
+
try {
|
|
21
|
+
const cmd = process.platform === 'win32' ? 'where claude' : 'which claude';
|
|
22
|
+
const result = execSync(cmd, { encoding: 'utf-8', windowsHide: true }).trim();
|
|
23
|
+
// 'where' on Windows may return multiple lines
|
|
24
|
+
const first = result.split('\n')[0].trim();
|
|
25
|
+
return path.dirname(first);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function register () {
|
|
32
|
+
const claudeDir = findClaudeDir();
|
|
33
|
+
if (!claudeDir) {
|
|
34
|
+
console.error('Could not find "claude" in PATH.');
|
|
35
|
+
console.error('Make sure Claude Code CLI is installed and available.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isWindows = process.platform === 'win32';
|
|
40
|
+
const created = [];
|
|
41
|
+
|
|
42
|
+
for (const [name, relPath] of Object.entries(BINS)) {
|
|
43
|
+
const scriptPath = path.join(PLUGIN_ROOT, relPath);
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(scriptPath)) {
|
|
46
|
+
console.warn(` Skip ${name}: ${scriptPath} not found`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (isWindows) {
|
|
51
|
+
const cmdFile = path.join(claudeDir, `${name}.cmd`);
|
|
52
|
+
const content = `@echo off\r\nnode "${scriptPath}" %*\r\n`;
|
|
53
|
+
fs.writeFileSync(cmdFile, content);
|
|
54
|
+
created.push(cmdFile);
|
|
55
|
+
} else {
|
|
56
|
+
const shFile = path.join(claudeDir, name);
|
|
57
|
+
const content = `#!/bin/sh\nexec node "${scriptPath}" "$@"\n`;
|
|
58
|
+
fs.writeFileSync(shFile, content, { mode: 0o755 });
|
|
59
|
+
created.push(shFile);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (created.length > 0) {
|
|
64
|
+
console.log('Registered CLI commands:');
|
|
65
|
+
for (const f of created) {
|
|
66
|
+
console.log(` ${f}`);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
console.log('No commands were registered.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function list () {
|
|
74
|
+
const claudeDir = findClaudeDir();
|
|
75
|
+
if (!claudeDir) {
|
|
76
|
+
console.log('Claude CLI not found in PATH.');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isWindows = process.platform === 'win32';
|
|
81
|
+
const ext = isWindows ? '.cmd' : '';
|
|
82
|
+
let found = 0;
|
|
83
|
+
|
|
84
|
+
for (const name of Object.keys(BINS)) {
|
|
85
|
+
const filePath = path.join(claudeDir, `${name}${ext}`);
|
|
86
|
+
const exists = fs.existsSync(filePath);
|
|
87
|
+
console.log(` ${exists ? '+' : '-'} ${name}${exists ? ` (${filePath})` : ''}`);
|
|
88
|
+
if (exists) {
|
|
89
|
+
found++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(`${found}/${Object.keys(BINS).length} commands registered in ${claudeDir}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function unregister () {
|
|
98
|
+
const claudeDir = findClaudeDir();
|
|
99
|
+
if (!claudeDir) {
|
|
100
|
+
console.log('Claude CLI not found in PATH — nothing to unregister.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const isWindows = process.platform === 'win32';
|
|
105
|
+
const removed = [];
|
|
106
|
+
|
|
107
|
+
for (const name of Object.keys(BINS)) {
|
|
108
|
+
const ext = isWindows ? '.cmd' : '';
|
|
109
|
+
const filePath = path.join(claudeDir, `${name}${ext}`);
|
|
110
|
+
if (fs.existsSync(filePath)) {
|
|
111
|
+
fs.unlinkSync(filePath);
|
|
112
|
+
removed.push(filePath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (removed.length > 0) {
|
|
117
|
+
console.log('Removed CLI commands:');
|
|
118
|
+
for (const f of removed) {
|
|
119
|
+
console.log(` ${f}`);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
console.log('No CLI commands found to remove.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const action = process.argv[2];
|
|
127
|
+
|
|
128
|
+
if (action === 'unregister' || action === 'remove') {
|
|
129
|
+
unregister();
|
|
130
|
+
} else if (action === 'list' || action === 'status') {
|
|
131
|
+
list();
|
|
132
|
+
} else if (action === 'register' || !action) {
|
|
133
|
+
register();
|
|
134
|
+
} else {
|
|
135
|
+
console.log('Usage: claude-notify-register <register|unregister|list>');
|
|
136
|
+
console.log('');
|
|
137
|
+
console.log('Commands:');
|
|
138
|
+
console.log(' register Register CLI commands next to claude binary (default)');
|
|
139
|
+
console.log(' unregister Remove registered CLI commands');
|
|
140
|
+
console.log(' list Show registration status of CLI commands');
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
package/bin/uninstall.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
6
7
|
|
|
7
8
|
const home = os.homedir();
|
|
8
9
|
const claudeDir = path.join(home, '.claude');
|
|
@@ -46,6 +47,26 @@ for (const file of [configPath, statePath]) {
|
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
// Remove CLI wrapper scripts
|
|
51
|
+
const CLI_BINS = ['claude-notify-listener', 'claude-notify-install', 'claude-notify-uninstall'];
|
|
52
|
+
let cliBinsRemoved = false;
|
|
53
|
+
try {
|
|
54
|
+
const cmd = process.platform === 'win32' ? 'where claude' : 'which claude';
|
|
55
|
+
const claudeBin = execSync(cmd, { encoding: 'utf-8', windowsHide: true }).trim().split('\n')[0].trim();
|
|
56
|
+
const claudeBinDir = path.dirname(claudeBin);
|
|
57
|
+
const ext = process.platform === 'win32' ? '.cmd' : '';
|
|
58
|
+
|
|
59
|
+
for (const name of CLI_BINS) {
|
|
60
|
+
const filePath = path.join(claudeBinDir, `${name}${ext}`);
|
|
61
|
+
if (fs.existsSync(filePath)) {
|
|
62
|
+
fs.unlinkSync(filePath);
|
|
63
|
+
cliBinsRemoved = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// claude not in PATH — nothing to remove
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
// Remove plugin cache (old versions left by Claude Code)
|
|
50
71
|
const pluginCacheDir = path.join(claudeDir, 'plugins', 'cache', 'bazilio-plugins', 'claude-notification-plugin');
|
|
51
72
|
let cacheRemoved = false;
|
|
@@ -61,4 +82,7 @@ console.log('Config files deleted.');
|
|
|
61
82
|
if (cacheRemoved) {
|
|
62
83
|
console.log('Plugin cache cleaned.');
|
|
63
84
|
}
|
|
85
|
+
if (cliBinsRemoved) {
|
|
86
|
+
console.log('CLI wrapper scripts removed.');
|
|
87
|
+
}
|
|
64
88
|
console.log('');
|
package/commands/setup.md
CHANGED
|
@@ -38,7 +38,15 @@ Help the user configure the notification plugin. The config file is `~/.claude/n
|
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
5. **
|
|
41
|
+
5. **Register CLI commands** — so that `claude-notify-listener`, `claude-notify-install`, and `claude-notify-uninstall` are available from the terminal (not just inside Claude).
|
|
42
|
+
|
|
43
|
+
Run in a shell:
|
|
44
|
+
```bash
|
|
45
|
+
node "${CLAUDE_PLUGIN_ROOT}/bin/register-cli.js"
|
|
46
|
+
```
|
|
47
|
+
This finds the directory where `claude` is installed and creates wrapper scripts there. Show the output to the user. If registration fails (e.g. `claude` not in PATH), inform the user but do NOT treat it as a fatal error — the rest of setup is still valid.
|
|
48
|
+
|
|
49
|
+
6. **Confirm** — show the user a summary of what was saved.
|
|
42
50
|
|
|
43
51
|
## Important
|
|
44
52
|
|
|
@@ -201,7 +201,8 @@ Running two listeners is impossible — the PID file prevents it. And this is im
|
|
|
201
201
|
| **WorkQueue** | `work-queue.js` | Manages task queues. Each working directory has a separate FIFO queue. Guarantees: one `claude` process per directory. Persists state to disk |
|
|
202
202
|
| **TaskRunner** | `task-runner.js` | Runs `claude -p "task"` as a child process. Monitors timeouts. Can kill the process on cancellation. Emits events: complete, error, timeout |
|
|
203
203
|
| **WorktreeManager** | `worktree-manager.js` | Creates and removes git worktrees. Auto-discovery via `git worktree list`. Maps `@project/branch` to a path on disk |
|
|
204
|
-
| **Logger** | `logger.js` | Writes log to `~/.claude/.listener.log`. Rotation when exceeding 5 MB (old file → `.log.old`) |
|
|
204
|
+
| **Logger** | `logger.js` | Writes operational log to `~/.claude/.cc-n-listener.log`. Rotation when exceeding 5 MB (old file → `.log.old`) |
|
|
205
|
+
| **TaskLogger** | `task-logger.js` | Writes task Q&A logs (questions to Claude and answers). Separate file per project/branch. Rotation at 5 MB |
|
|
205
206
|
|
|
206
207
|
---
|
|
207
208
|
|
|
@@ -304,7 +305,9 @@ Full example of `~/.claude/notifier.config.json` with the listener section:
|
|
|
304
305
|
"autoCreateWorktree": true,
|
|
305
306
|
"taskTimeoutMinutes": 30,
|
|
306
307
|
"maxQueuePerWorkDir": 10,
|
|
307
|
-
"maxTotalTasks": 50
|
|
308
|
+
"maxTotalTasks": 50,
|
|
309
|
+
"logDir": "~/.claude",
|
|
310
|
+
"taskLogDir": "~/.claude"
|
|
308
311
|
}
|
|
309
312
|
}
|
|
310
313
|
```
|
|
@@ -319,6 +322,8 @@ Full example of `~/.claude/notifier.config.json` with the listener section:
|
|
|
319
322
|
| `taskTimeoutMinutes` | `30` | Maximum task execution time in minutes. Force-stopped when exceeded |
|
|
320
323
|
| `maxQueuePerWorkDir` | `10` | Maximum tasks in the queue for a single working directory |
|
|
321
324
|
| `maxTotalTasks` | `50` | Maximum tasks across all queues combined |
|
|
325
|
+
| `logDir` | `~/.claude` | Directory for the listener operational log (`.cc-n-listener.log`) |
|
|
326
|
+
| `taskLogDir` | same as `logDir` | Directory for task Q&A logs (`.cc-n-task-*.log`). Each project/branch gets its own file |
|
|
322
327
|
|
|
323
328
|
### What is `projects`?
|
|
324
329
|
|
|
@@ -392,7 +397,7 @@ If the worktree doesn't exist, it will be created automatically.
|
|
|
392
397
|
5. When Claude finishes → sends the result to Telegram
|
|
393
398
|
6. If there's a next task in the queue → starts it
|
|
394
399
|
|
|
395
|
-
---
|
|
400
|
+
---RFr
|
|
396
401
|
|
|
397
402
|
## Projects and worktrees
|
|
398
403
|
|
|
@@ -750,6 +755,44 @@ Handling long responses:
|
|
|
750
755
|
|
|
751
756
|
---
|
|
752
757
|
|
|
758
|
+
## Logs
|
|
759
|
+
|
|
760
|
+
The listener writes two types of logs:
|
|
761
|
+
|
|
762
|
+
### Listener log (operational)
|
|
763
|
+
|
|
764
|
+
Internal events: startup, incoming messages, task lifecycle, errors.
|
|
765
|
+
**Does NOT contain** Claude's questions and answers.
|
|
766
|
+
|
|
767
|
+
Default path: `~/.claude/.cc-n-listener.log`
|
|
768
|
+
Customizable via `listener.logDir` in config.
|
|
769
|
+
Rotation: 5 MB (old file → `.log.old`).
|
|
770
|
+
|
|
771
|
+
### Task logs (Q&A with Claude)
|
|
772
|
+
|
|
773
|
+
Each project/branch gets its own log file with full questions sent to Claude and answers received.
|
|
774
|
+
|
|
775
|
+
Default directory: same as `listener.logDir` (i.e. `~/.claude/`)
|
|
776
|
+
Customizable via `listener.taskLogDir` in config.
|
|
777
|
+
Filename pattern: `.cc-n-task-<project>[_<branch>].log`
|
|
778
|
+
Rotation: 5 MB per file (old file → `.log.old`).
|
|
779
|
+
|
|
780
|
+
Each entry includes a timestamp, working directory, task text (question), and Claude's full response (answer).
|
|
781
|
+
|
|
782
|
+
Example config to customize both log directories:
|
|
783
|
+
|
|
784
|
+
```json
|
|
785
|
+
{
|
|
786
|
+
"listener": {
|
|
787
|
+
"logDir": "/var/log/claude-listener",
|
|
788
|
+
"taskLogDir": "/var/log/claude-listener/tasks",
|
|
789
|
+
"projects": { ... }
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
753
796
|
## State files
|
|
754
797
|
|
|
755
798
|
All files are stored in `~/.claude/`:
|
|
@@ -757,7 +800,8 @@ All files are stored in `~/.claude/`:
|
|
|
757
800
|
| File | Description |
|
|
758
801
|
|---|---|
|
|
759
802
|
| `.listener.pid` | PID of the running daemon. On `start`, it checks whether the process is alive |
|
|
760
|
-
| `.listener.log` |
|
|
803
|
+
| `.cc-n-listener.log` | Operational log. Rotation when exceeding 5 MB (old file → `.log.old`) |
|
|
804
|
+
| `.cc-n-task-*.log` | Task Q&A logs, one per project/branch. Rotation at 5 MB each |
|
|
761
805
|
| `.task_queues.json` | Current state of all queues. Persisted to disk after every change |
|
|
762
806
|
| `.task_history.json` | Last 50 completed tasks (for `/history`) |
|
|
763
807
|
|
package/listener/listener.js
CHANGED
|
@@ -5,6 +5,7 @@ import os from 'os';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import process from 'process';
|
|
7
7
|
import { createLogger } from './logger.js';
|
|
8
|
+
import { createTaskLogger } from './task-logger.js';
|
|
8
9
|
import { TelegramPoller, escapeHtml } from './telegram-poller.js';
|
|
9
10
|
import { WorkQueue } from './work-queue.js';
|
|
10
11
|
import { TaskRunner } from './task-runner.js';
|
|
@@ -16,7 +17,7 @@ import { parseMessage, parseTarget } from './message-parser.js';
|
|
|
16
17
|
// ----------------------
|
|
17
18
|
|
|
18
19
|
const CONFIG_PATH = path.join(os.homedir(), '.claude', 'notifier.config.json');
|
|
19
|
-
const
|
|
20
|
+
const DEFAULT_LOG_DIR = path.join(os.homedir(), '.claude');
|
|
20
21
|
|
|
21
22
|
function loadConfig () {
|
|
22
23
|
try {
|
|
@@ -33,7 +34,9 @@ function loadConfig () {
|
|
|
33
34
|
// ----------------------
|
|
34
35
|
|
|
35
36
|
const config = loadConfig();
|
|
36
|
-
const
|
|
37
|
+
const listenerLogDir = config.listener?.logDir || DEFAULT_LOG_DIR;
|
|
38
|
+
fs.mkdirSync(listenerLogDir, { recursive: true });
|
|
39
|
+
const logger = createLogger(path.join(listenerLogDir, '.cc-n-listener.log'));
|
|
37
40
|
|
|
38
41
|
// Validate required fields
|
|
39
42
|
const token = process.env.CLAUDE_NOTIFY_TELEGRAM_TOKEN || config.telegramToken || config.telegram?.token;
|
|
@@ -61,7 +64,10 @@ const queue = new WorkQueue(
|
|
|
61
64
|
listenerConfig.maxQueuePerWorkDir || 10,
|
|
62
65
|
listenerConfig.maxTotalTasks || 50,
|
|
63
66
|
);
|
|
64
|
-
const
|
|
67
|
+
const taskLogDir = config.listener?.taskLogDir || listenerLogDir;
|
|
68
|
+
fs.mkdirSync(taskLogDir, { recursive: true });
|
|
69
|
+
const taskLogger = createTaskLogger(taskLogDir);
|
|
70
|
+
const runner = new TaskRunner(logger, taskTimeout, taskLogger);
|
|
65
71
|
const worktreeManager = new WorktreeManager(config, logger);
|
|
66
72
|
|
|
67
73
|
const startTime = Date.now();
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
|
|
7
|
+
|
|
8
|
+
function sanitize (str) {
|
|
9
|
+
return str.replace(/[/\\:*?"<>|\s]+/g, '_').replace(/^_+|_+$/g, '');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function rotateIfNeeded (filePath) {
|
|
13
|
+
try {
|
|
14
|
+
const stat = fs.statSync(filePath);
|
|
15
|
+
if (stat.size > MAX_LOG_SIZE) {
|
|
16
|
+
const backup = filePath + '.old';
|
|
17
|
+
if (fs.existsSync(backup)) {
|
|
18
|
+
fs.unlinkSync(backup);
|
|
19
|
+
}
|
|
20
|
+
fs.renameSync(filePath, backup);
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// file doesn't exist yet
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a task logger that writes Q&A logs for Claude tasks.
|
|
29
|
+
* Each project/branch combo gets its own log file.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} logDir - Directory for task log files
|
|
32
|
+
*/
|
|
33
|
+
export function createTaskLogger (logDir) {
|
|
34
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
function getLogPath (project, branch) {
|
|
37
|
+
const parts = [project];
|
|
38
|
+
if (branch && branch !== 'main' && branch !== 'master') {
|
|
39
|
+
parts.push(branch);
|
|
40
|
+
}
|
|
41
|
+
const name = `.cc-n-task-${sanitize(parts.join('_'))}.log`;
|
|
42
|
+
return path.join(logDir, name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
logQuestion (project, branch, workDir, taskText) {
|
|
47
|
+
const logPath = getLogPath(project, branch);
|
|
48
|
+
rotateIfNeeded(logPath);
|
|
49
|
+
const ts = new Date().toISOString();
|
|
50
|
+
const entry = `\n${'='.repeat(80)}\n`
|
|
51
|
+
+ `[${ts}] QUESTION\n`
|
|
52
|
+
+ `Project: @${project}${branch && branch !== 'main' && branch !== 'master' ? '/' + branch : ''}\n`
|
|
53
|
+
+ `WorkDir: ${workDir}\n`
|
|
54
|
+
+ `Task: ${taskText}\n`;
|
|
55
|
+
fs.appendFileSync(logPath, entry);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
logAnswer (project, branch, output, exitCode) {
|
|
59
|
+
const logPath = getLogPath(project, branch);
|
|
60
|
+
rotateIfNeeded(logPath);
|
|
61
|
+
const ts = new Date().toISOString();
|
|
62
|
+
const status = exitCode === 0 ? 'OK' : `ERROR (code ${exitCode})`;
|
|
63
|
+
const entry = `[${ts}] ANSWER [${status}]\n`
|
|
64
|
+
+ `${output || '(no output)'}\n`;
|
|
65
|
+
fs.appendFileSync(logPath, entry);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
package/listener/task-runner.js
CHANGED
|
@@ -9,10 +9,11 @@ const DEFAULT_TIMEOUT = 600_000; // 10 minutes
|
|
|
9
9
|
* Runs claude CLI tasks and emits events on completion.
|
|
10
10
|
*/
|
|
11
11
|
export class TaskRunner extends EventEmitter {
|
|
12
|
-
constructor (logger, timeout) {
|
|
12
|
+
constructor (logger, timeout, taskLogger) {
|
|
13
13
|
super();
|
|
14
14
|
this.logger = logger;
|
|
15
15
|
this.timeout = timeout || DEFAULT_TIMEOUT;
|
|
16
|
+
this.taskLogger = taskLogger || null;
|
|
16
17
|
this.activeProcesses = new Map(); // workDir → { child, timer, task }
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -28,6 +29,9 @@ export class TaskRunner extends EventEmitter {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
this.logger.info(`Running task "${task.text}" in ${workDir}`);
|
|
32
|
+
if (this.taskLogger) {
|
|
33
|
+
this.taskLogger.logQuestion(task.project || 'unknown', task.branch || 'main', workDir, task.text);
|
|
34
|
+
}
|
|
31
35
|
|
|
32
36
|
const args = ['-p', task.text, '--output-format', 'text'];
|
|
33
37
|
const child = spawn('claude', args, {
|
|
@@ -35,6 +39,7 @@ export class TaskRunner extends EventEmitter {
|
|
|
35
39
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
40
|
windowsHide: true,
|
|
37
41
|
shell: process.platform === 'win32',
|
|
42
|
+
env: { ...process.env, CLAUDE_NOTIFY_FROM_LISTENER: '1' },
|
|
38
43
|
});
|
|
39
44
|
|
|
40
45
|
let stdout = '';
|
|
@@ -65,10 +70,16 @@ export class TaskRunner extends EventEmitter {
|
|
|
65
70
|
|
|
66
71
|
if (code === 0) {
|
|
67
72
|
this.logger.info(`Task "${task.id}" completed in ${workDir}`);
|
|
73
|
+
if (this.taskLogger) {
|
|
74
|
+
this.taskLogger.logAnswer(task.project || 'unknown', task.branch || 'main', stdout.trim(), 0);
|
|
75
|
+
}
|
|
68
76
|
this.emit('complete', workDir, task, stdout.trim());
|
|
69
77
|
} else {
|
|
70
78
|
const errorMsg = stderr.trim() || `Process exited with code ${code}`;
|
|
71
79
|
this.logger.error(`Task "${task.id}" failed in ${workDir}: ${errorMsg}`);
|
|
80
|
+
if (this.taskLogger) {
|
|
81
|
+
this.taskLogger.logAnswer(task.project || 'unknown', task.branch || 'main', errorMsg, code);
|
|
82
|
+
}
|
|
72
83
|
this.emit('error', workDir, task, errorMsg);
|
|
73
84
|
}
|
|
74
85
|
});
|
package/listener/work-queue.js
CHANGED
package/notifier/notifier.js
CHANGED
|
@@ -153,8 +153,16 @@ function loadConfig () {
|
|
|
153
153
|
// ----------------------
|
|
154
154
|
|
|
155
155
|
function isNotifierDisabled () {
|
|
156
|
-
|
|
157
|
-
|| process.env.CLAUDE_NOTIFY_DISABLE === 'true'
|
|
156
|
+
if (process.env.CLAUDE_NOTIFY_DISABLE === '1'
|
|
157
|
+
|| process.env.CLAUDE_NOTIFY_DISABLE === 'true') {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
// Skip notifications for listener-spawned tasks unless explicitly enabled
|
|
161
|
+
if (process.env.CLAUDE_NOTIFY_FROM_LISTENER === '1'
|
|
162
|
+
&& process.env.CLAUDE_NOTIFY_AFTER_LISTENER !== '1') {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
// ----------------------
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
3
|
"productName": "claude-notification-plugin",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.80",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"claude-notify-install": "bin/install.js",
|
|
22
22
|
"claude-notify-uninstall": "bin/uninstall.js",
|
|
23
23
|
"claude-notifier": "notifier/notifier.js",
|
|
24
|
-
"claude-notify-listener": "bin/listener-cli.js"
|
|
24
|
+
"claude-notify-listener": "bin/listener-cli.js",
|
|
25
|
+
"claude-notify-register": "bin/register-cli.js"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
27
28
|
"lint": "eslint .",
|