claude-notification-plugin 1.0.59 → 1.0.63
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 +42 -54
- 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,279 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Manages git worktrees for projects.
|
|
10
|
+
*/
|
|
11
|
+
export class WorktreeManager {
|
|
12
|
+
constructor (config, logger) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
const baseDir = config.listener?.worktreeBaseDir || path.join(os.homedir(), '.claude', 'worktrees');
|
|
16
|
+
this.baseDir = baseDir.replace(/^~/, os.homedir());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Discover existing worktrees for a project by running `git worktree list`.
|
|
21
|
+
* Updates config.listener.projects[alias].worktrees in-place.
|
|
22
|
+
*/
|
|
23
|
+
discoverWorktrees (projectAlias) {
|
|
24
|
+
const project = this.config.listener?.projects?.[projectAlias];
|
|
25
|
+
if (!project?.path) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
const projectPath = this._resolvePath(project.path);
|
|
29
|
+
if (!fs.existsSync(projectPath)) {
|
|
30
|
+
return project.worktrees || {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const output = execSync('git worktree list --porcelain', {
|
|
35
|
+
cwd: projectPath,
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
timeout: 5000,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const worktrees = {};
|
|
42
|
+
let currentPath = null;
|
|
43
|
+
let currentBranch = null;
|
|
44
|
+
|
|
45
|
+
for (const line of output.split('\n')) {
|
|
46
|
+
if (line.startsWith('worktree ')) {
|
|
47
|
+
currentPath = line.substring('worktree '.length).trim();
|
|
48
|
+
currentBranch = null;
|
|
49
|
+
} else if (line.startsWith('branch ')) {
|
|
50
|
+
const ref = line.substring('branch '.length).trim();
|
|
51
|
+
// refs/heads/feature/auth → feature/auth
|
|
52
|
+
currentBranch = ref.replace(/^refs\/heads\//, '');
|
|
53
|
+
} else if (line === '' && currentPath && currentBranch) {
|
|
54
|
+
// Skip the main worktree (same path as project)
|
|
55
|
+
const normalizedCurrent = path.resolve(currentPath);
|
|
56
|
+
const normalizedProject = path.resolve(projectPath);
|
|
57
|
+
if (normalizedCurrent !== normalizedProject) {
|
|
58
|
+
worktrees[currentBranch] = currentPath;
|
|
59
|
+
}
|
|
60
|
+
currentPath = null;
|
|
61
|
+
currentBranch = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle last entry
|
|
66
|
+
if (currentPath && currentBranch) {
|
|
67
|
+
const normalizedCurrent = path.resolve(currentPath);
|
|
68
|
+
const normalizedProject = path.resolve(projectPath);
|
|
69
|
+
if (normalizedCurrent !== normalizedProject) {
|
|
70
|
+
worktrees[currentBranch] = currentPath;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Merge with existing config (keep manually configured paths)
|
|
75
|
+
if (!project.worktrees) {
|
|
76
|
+
project.worktrees = {};
|
|
77
|
+
}
|
|
78
|
+
for (const [branch, wtPath] of Object.entries(worktrees)) {
|
|
79
|
+
if (!project.worktrees[branch]) {
|
|
80
|
+
project.worktrees[branch] = wtPath;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.logger.info(`Discovered worktrees for "${projectAlias}": ${JSON.stringify(worktrees)}`);
|
|
85
|
+
return project.worktrees;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
this.logger.error(`Failed to discover worktrees for "${projectAlias}": ${err.message}`);
|
|
88
|
+
return project.worktrees || {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a new worktree for a project.
|
|
94
|
+
*/
|
|
95
|
+
createWorktree (projectAlias, branch) {
|
|
96
|
+
const project = this.config.listener?.projects?.[projectAlias];
|
|
97
|
+
if (!project?.path) {
|
|
98
|
+
throw new Error(`Project "${projectAlias}" not found`);
|
|
99
|
+
}
|
|
100
|
+
const projectPath = this._resolvePath(project.path);
|
|
101
|
+
if (!fs.existsSync(projectPath)) {
|
|
102
|
+
throw new Error(`Project path does not exist: ${projectPath}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Sanitize branch for directory name
|
|
106
|
+
const dirName = branch.replace(/\//g, '-');
|
|
107
|
+
const wtDir = path.join(this.baseDir, projectAlias, dirName);
|
|
108
|
+
|
|
109
|
+
if (fs.existsSync(wtDir)) {
|
|
110
|
+
// Worktree directory already exists — just register it
|
|
111
|
+
if (!project.worktrees) {
|
|
112
|
+
project.worktrees = {};
|
|
113
|
+
}
|
|
114
|
+
project.worktrees[branch] = wtDir;
|
|
115
|
+
return wtDir;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fs.mkdirSync(path.dirname(wtDir), { recursive: true });
|
|
119
|
+
|
|
120
|
+
// Check if branch exists
|
|
121
|
+
let branchExists = false;
|
|
122
|
+
try {
|
|
123
|
+
execSync(`git rev-parse --verify "${branch}"`, {
|
|
124
|
+
cwd: projectPath,
|
|
125
|
+
encoding: 'utf-8',
|
|
126
|
+
windowsHide: true,
|
|
127
|
+
stdio: 'pipe',
|
|
128
|
+
timeout: 5000,
|
|
129
|
+
});
|
|
130
|
+
branchExists = true;
|
|
131
|
+
} catch {
|
|
132
|
+
// branch doesn't exist
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if (branchExists) {
|
|
137
|
+
execSync(`git worktree add "${wtDir}" "${branch}"`, {
|
|
138
|
+
cwd: projectPath,
|
|
139
|
+
encoding: 'utf-8',
|
|
140
|
+
windowsHide: true,
|
|
141
|
+
stdio: 'pipe',
|
|
142
|
+
timeout: 30000,
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
execSync(`git worktree add -b "${branch}" "${wtDir}"`, {
|
|
146
|
+
cwd: projectPath,
|
|
147
|
+
encoding: 'utf-8',
|
|
148
|
+
windowsHide: true,
|
|
149
|
+
stdio: 'pipe',
|
|
150
|
+
timeout: 30000,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
throw new Error(`Failed to create worktree: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!project.worktrees) {
|
|
158
|
+
project.worktrees = {};
|
|
159
|
+
}
|
|
160
|
+
project.worktrees[branch] = wtDir;
|
|
161
|
+
|
|
162
|
+
this.logger.info(`Created worktree for "${projectAlias}": ${branch} → ${wtDir}`);
|
|
163
|
+
return wtDir;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Remove a worktree.
|
|
168
|
+
*/
|
|
169
|
+
removeWorktree (projectAlias, branch) {
|
|
170
|
+
const project = this.config.listener?.projects?.[projectAlias];
|
|
171
|
+
if (!project?.path) {
|
|
172
|
+
throw new Error(`Project "${projectAlias}" not found`);
|
|
173
|
+
}
|
|
174
|
+
const projectPath = this._resolvePath(project.path);
|
|
175
|
+
|
|
176
|
+
const wtDir = project.worktrees?.[branch];
|
|
177
|
+
if (!wtDir) {
|
|
178
|
+
throw new Error(`Worktree "${branch}" not found for project "${projectAlias}"`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
execSync(`git worktree remove "${wtDir}" --force`, {
|
|
183
|
+
cwd: projectPath,
|
|
184
|
+
encoding: 'utf-8',
|
|
185
|
+
windowsHide: true,
|
|
186
|
+
stdio: 'pipe',
|
|
187
|
+
timeout: 10000,
|
|
188
|
+
});
|
|
189
|
+
} catch (err) {
|
|
190
|
+
this.logger.warn(`git worktree remove failed: ${err.message}, removing directory manually`);
|
|
191
|
+
try {
|
|
192
|
+
fs.rmSync(wtDir, { recursive: true, force: true });
|
|
193
|
+
execSync('git worktree prune', {
|
|
194
|
+
cwd: projectPath,
|
|
195
|
+
encoding: 'utf-8',
|
|
196
|
+
windowsHide: true,
|
|
197
|
+
stdio: 'pipe',
|
|
198
|
+
timeout: 5000,
|
|
199
|
+
});
|
|
200
|
+
} catch (err2) {
|
|
201
|
+
throw new Error(`Failed to remove worktree directory: ${err2.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
delete project.worktrees[branch];
|
|
206
|
+
this.logger.info(`Removed worktree for "${projectAlias}": ${branch}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* List all worktrees for a project (including main).
|
|
211
|
+
*/
|
|
212
|
+
listWorktrees (projectAlias) {
|
|
213
|
+
const project = this.config.listener?.projects?.[projectAlias];
|
|
214
|
+
if (!project?.path) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Discover fresh from git
|
|
219
|
+
this.discoverWorktrees(projectAlias);
|
|
220
|
+
|
|
221
|
+
const result = {
|
|
222
|
+
main: this._resolvePath(project.path),
|
|
223
|
+
worktrees: {},
|
|
224
|
+
};
|
|
225
|
+
if (project.worktrees) {
|
|
226
|
+
for (const [branch, wtPath] of Object.entries(project.worktrees)) {
|
|
227
|
+
result.worktrees[branch] = this._resolvePath(wtPath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Resolve a workDir for a project + optional branch.
|
|
235
|
+
* If branch is null, returns main worktree path.
|
|
236
|
+
* If autoCreateWorktree is true, creates missing worktrees.
|
|
237
|
+
*/
|
|
238
|
+
resolveWorkDir (projectAlias, branch) {
|
|
239
|
+
const project = this.config.listener?.projects?.[projectAlias];
|
|
240
|
+
if (!project?.path) {
|
|
241
|
+
throw new Error(`Project "${projectAlias}" not found. Use /projects to see available projects.`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!branch) {
|
|
245
|
+
return this._resolvePath(project.path);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check existing worktrees
|
|
249
|
+
if (project.worktrees?.[branch]) {
|
|
250
|
+
const resolved = this._resolvePath(project.worktrees[branch]);
|
|
251
|
+
if (fs.existsSync(resolved)) {
|
|
252
|
+
return resolved;
|
|
253
|
+
}
|
|
254
|
+
// Path no longer exists, clean up
|
|
255
|
+
delete project.worktrees[branch];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Try auto-discover
|
|
259
|
+
this.discoverWorktrees(projectAlias);
|
|
260
|
+
if (project.worktrees?.[branch]) {
|
|
261
|
+
return this._resolvePath(project.worktrees[branch]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Auto-create if enabled
|
|
265
|
+
const autoCreate = this.config.listener?.autoCreateWorktree !== false;
|
|
266
|
+
if (autoCreate) {
|
|
267
|
+
return this.createWorktree(projectAlias, branch);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Worktree "${branch}" not found for project "${projectAlias}". `
|
|
272
|
+
+ `Create it: /worktree @${projectAlias} ${branch}`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_resolvePath (p) {
|
|
277
|
+
return path.resolve(p.replace(/^~/, os.homedir()));
|
|
278
|
+
}
|
|
279
|
+
}
|
package/notifier/notifier.js
CHANGED
|
@@ -678,11 +678,13 @@ process.stdin.on('end', async () => {
|
|
|
678
678
|
const branchLine = branch ? `\nBranch: ${branch}` : '';
|
|
679
679
|
const branchLineHtml = branch ? `\nBranch: <b>${escapeHtml(branch)}</b>` : '';
|
|
680
680
|
|
|
681
|
+
const triggerLine = config.debug ? `\nTrigger: ${eventType}` : '';
|
|
682
|
+
|
|
681
683
|
let message =
|
|
682
|
-
`${title}\n\nProject: ${project}${branchLine}\nDuration: ${duration}s
|
|
684
|
+
`${title}\n\nProject: ${project}${branchLine}\nDuration: ${duration}s${triggerLine}`;
|
|
683
685
|
|
|
684
686
|
let telegramMessage =
|
|
685
|
-
`${escapeHtml(title)}\n\nProject: <b>${escapeHtml(project)}</b>${branchLineHtml}\nDuration: ${duration}s
|
|
687
|
+
`${escapeHtml(title)}\n\nProject: <b>${escapeHtml(project)}</b>${branchLineHtml}\nDuration: ${duration}s${triggerLine}`;
|
|
686
688
|
|
|
687
689
|
if (config.telegram.includeLastCcMessageInTelegram && event.last_assistant_message) {
|
|
688
690
|
const maxLen = 3500;
|
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.63",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"bin/",
|
|
13
13
|
"commands/",
|
|
14
14
|
"hooks/",
|
|
15
|
+
"listener/",
|
|
15
16
|
"notifier/",
|
|
16
17
|
"README.md",
|
|
17
18
|
"LICENSE"
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
"bin": {
|
|
20
21
|
"claude-notify-install": "bin/install.js",
|
|
21
22
|
"claude-notify-uninstall": "bin/uninstall.js",
|
|
22
|
-
"claude-notifier": "notifier/notifier.js"
|
|
23
|
+
"claude-notifier": "notifier/notifier.js",
|
|
24
|
+
"claude-notify-listener": "bin/listener-cli.js"
|
|
23
25
|
},
|
|
24
26
|
"scripts": {
|
|
25
27
|
"lint": "eslint .",
|