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.
@@ -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
+ }
@@ -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\nTrigger: ${eventType}`;
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\nTrigger: ${eventType}`;
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.59",
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 .",