claude-notification-plugin 1.0.80 → 1.0.88
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 +13 -14
- package/README.md +276 -283
- package/bin/install.js +262 -16
- package/bin/uninstall.js +3 -2
- package/listener/LISTENER-DETAILED.md +3 -3
- package/listener/listener.js +39 -16
- package/listener/telegram-poller.js +27 -1
- package/package.json +60 -60
- package/bin/register-cli.js +0 -142
- package/commands/listener.md +0 -100
- package/commands/setup.md +0 -54
package/bin/install.js
CHANGED
|
@@ -4,17 +4,246 @@ import fs from 'fs';
|
|
|
4
4
|
import os from 'os';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import readline from 'readline';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
7
12
|
|
|
8
13
|
const home = os.homedir();
|
|
9
14
|
const claudeDir = path.join(home, '.claude');
|
|
15
|
+
const pluginsDir = path.join(claudeDir, 'plugins');
|
|
10
16
|
const configPath = path.join(claudeDir, 'notifier.config.json');
|
|
11
17
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
18
|
+
const installedPluginsPath = path.join(pluginsDir, 'installed_plugins.json');
|
|
19
|
+
const knownMarketplacesPath = path.join(pluginsDir, 'known_marketplaces.json');
|
|
20
|
+
const marketplacesDir = path.join(pluginsDir, 'marketplaces');
|
|
21
|
+
const RESOLVER_PATH = path.join(claudeDir, 'claude-notify-resolve.js');
|
|
12
22
|
|
|
13
23
|
const HOOK_COMMAND = 'claude-notifier';
|
|
24
|
+
const MARKETPLACE_KEY = 'bazilio-plugins';
|
|
25
|
+
const PLUGIN_KEY = 'claude-notification-plugin@bazilio-plugins';
|
|
26
|
+
const MARKETPLACE_REPO = 'https://github.com/Bazilio-san/claude-plugins.git';
|
|
27
|
+
const MARKETPLACE_GITHUB = 'Bazilio-san/claude-plugins';
|
|
28
|
+
|
|
29
|
+
const CLI_BINS = {
|
|
30
|
+
'claude-notify-listener': 'bin/listener-cli.js',
|
|
31
|
+
'claude-notify-install': 'bin/install.js',
|
|
32
|
+
'claude-notify-uninstall': 'bin/uninstall.js',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ──────────────────────────────────────
|
|
36
|
+
// Plugin registration
|
|
37
|
+
// ──────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
function getVersion () {
|
|
40
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
41
|
+
return pkg.version;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function copyToCache (version) {
|
|
45
|
+
const cacheBase = path.join(pluginsDir, 'cache', MARKETPLACE_KEY, 'claude-notification-plugin');
|
|
46
|
+
const dest = path.join(cacheBase, version);
|
|
47
|
+
|
|
48
|
+
if (fs.existsSync(dest)) {
|
|
49
|
+
fs.rmSync(dest, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
52
|
+
|
|
53
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
|
|
54
|
+
const entries = [...(pkg.files || []), 'package.json', 'package-lock.json'];
|
|
55
|
+
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const src = path.join(PACKAGE_ROOT, entry);
|
|
58
|
+
if (!fs.existsSync(src)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const target = path.join(dest, entry);
|
|
62
|
+
const stat = fs.statSync(src);
|
|
63
|
+
if (stat.isDirectory()) {
|
|
64
|
+
fs.cpSync(src, target, { recursive: true });
|
|
65
|
+
} else {
|
|
66
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
67
|
+
fs.copyFileSync(src, target);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
execSync('npm install --production --ignore-scripts', {
|
|
73
|
+
cwd: dest,
|
|
74
|
+
stdio: 'pipe',
|
|
75
|
+
windowsHide: true,
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
console.warn(' Warning: could not install dependencies in plugin cache.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return dest;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function updateInstalledPlugins (version, installPath) {
|
|
85
|
+
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
86
|
+
|
|
87
|
+
let data = { version: 2, plugins: {} };
|
|
88
|
+
if (fs.existsSync(installedPluginsPath)) {
|
|
89
|
+
try {
|
|
90
|
+
data = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8'));
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore malformed file
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const now = new Date().toISOString();
|
|
97
|
+
const existing = data.plugins[PLUGIN_KEY]?.[0];
|
|
98
|
+
|
|
99
|
+
data.plugins[PLUGIN_KEY] = [{
|
|
100
|
+
scope: 'user',
|
|
101
|
+
installPath,
|
|
102
|
+
version,
|
|
103
|
+
installedAt: existing?.installedAt || now,
|
|
104
|
+
lastUpdated: now,
|
|
105
|
+
gitCommitSha: existing?.gitCommitSha || '',
|
|
106
|
+
}];
|
|
107
|
+
|
|
108
|
+
fs.writeFileSync(installedPluginsPath, JSON.stringify(data, null, 2));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function updateKnownMarketplaces () {
|
|
112
|
+
let data = {};
|
|
113
|
+
if (fs.existsSync(knownMarketplacesPath)) {
|
|
114
|
+
try {
|
|
115
|
+
data = JSON.parse(fs.readFileSync(knownMarketplacesPath, 'utf-8'));
|
|
116
|
+
} catch {
|
|
117
|
+
// ignore malformed file
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const installLocation = path.join(marketplacesDir, MARKETPLACE_KEY);
|
|
122
|
+
|
|
123
|
+
data[MARKETPLACE_KEY] = {
|
|
124
|
+
...data[MARKETPLACE_KEY],
|
|
125
|
+
source: {
|
|
126
|
+
source: 'github',
|
|
127
|
+
repo: MARKETPLACE_GITHUB,
|
|
128
|
+
},
|
|
129
|
+
installLocation,
|
|
130
|
+
lastUpdated: data[MARKETPLACE_KEY]?.lastUpdated || new Date().toISOString(),
|
|
131
|
+
autoUpdate: true,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
fs.writeFileSync(knownMarketplacesPath, JSON.stringify(data, null, 2));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cloneOrUpdateMarketplace () {
|
|
138
|
+
const dest = path.join(marketplacesDir, MARKETPLACE_KEY);
|
|
139
|
+
|
|
140
|
+
if (fs.existsSync(path.join(dest, '.git'))) {
|
|
141
|
+
try {
|
|
142
|
+
execSync('git pull --ff-only', { cwd: dest, stdio: 'pipe', windowsHide: true });
|
|
143
|
+
} catch {
|
|
144
|
+
// offline or conflict — not fatal
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
fs.mkdirSync(marketplacesDir, { recursive: true });
|
|
148
|
+
try {
|
|
149
|
+
execSync(`git clone "${MARKETPLACE_REPO}" "${dest}"`, {
|
|
150
|
+
stdio: 'pipe',
|
|
151
|
+
windowsHide: true,
|
|
152
|
+
});
|
|
153
|
+
} catch {
|
|
154
|
+
console.warn(' Warning: could not clone marketplace repo (offline?).');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ──────────────────────────────────────
|
|
160
|
+
// CLI wrapper registration
|
|
161
|
+
// ──────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function writeResolver () {
|
|
164
|
+
const content = `#!/usr/bin/env node
|
|
165
|
+
'use strict';
|
|
166
|
+
const fs = require('fs');
|
|
167
|
+
const path = require('path');
|
|
168
|
+
const { execFileSync } = require('child_process');
|
|
169
|
+
|
|
170
|
+
const pluginKey = 'claude-notification-plugin@bazilio-plugins';
|
|
171
|
+
const home = process.env.USERPROFILE || process.env.HOME;
|
|
172
|
+
const regPath = path.join(home, '.claude', 'plugins', 'installed_plugins.json');
|
|
173
|
+
|
|
174
|
+
let installPath;
|
|
175
|
+
try {
|
|
176
|
+
const reg = JSON.parse(fs.readFileSync(regPath, 'utf-8'));
|
|
177
|
+
const entries = reg.plugins[pluginKey];
|
|
178
|
+
if (entries && entries.length) installPath = entries[0].installPath;
|
|
179
|
+
} catch {}
|
|
180
|
+
|
|
181
|
+
if (!installPath) {
|
|
182
|
+
console.error('claude-notification-plugin is not installed.');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
14
185
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
186
|
+
const target = path.join(installPath, process.argv[2]);
|
|
187
|
+
const args = process.argv.slice(3);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
execFileSync('node', [target, ...args], { stdio: 'inherit' });
|
|
191
|
+
} catch (e) {
|
|
192
|
+
process.exit(e.status || 1);
|
|
193
|
+
}
|
|
194
|
+
`;
|
|
195
|
+
fs.writeFileSync(RESOLVER_PATH, content, { mode: 0o755 });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function findClaudeDir () {
|
|
199
|
+
try {
|
|
200
|
+
const cmd = process.platform === 'win32' ? 'where claude' : 'which claude';
|
|
201
|
+
const result = execSync(cmd, { encoding: 'utf-8', windowsHide: true }).trim();
|
|
202
|
+
const first = result.split('\n')[0].trim();
|
|
203
|
+
return path.dirname(first);
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function registerCli () {
|
|
210
|
+
const binDir = findClaudeDir();
|
|
211
|
+
if (!binDir) {
|
|
212
|
+
console.warn(' Warning: "claude" not found in PATH — CLI wrappers not registered.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
writeResolver();
|
|
217
|
+
|
|
218
|
+
const isWindows = process.platform === 'win32';
|
|
219
|
+
const resolverNative = RESOLVER_PATH.replace(/\//g, path.sep);
|
|
220
|
+
const created = [];
|
|
221
|
+
|
|
222
|
+
for (const [name, relPath] of Object.entries(CLI_BINS)) {
|
|
223
|
+
if (isWindows) {
|
|
224
|
+
const cmdFile = path.join(binDir, `${name}.cmd`);
|
|
225
|
+
const content = `@echo off\r\nnode "${resolverNative}" ${relPath} %*\r\n`;
|
|
226
|
+
fs.writeFileSync(cmdFile, content);
|
|
227
|
+
created.push(cmdFile);
|
|
228
|
+
} else {
|
|
229
|
+
const shFile = path.join(binDir, name);
|
|
230
|
+
const content = `#!/bin/sh\nexec node "${RESOLVER_PATH}" "${relPath}" "$@"\n`;
|
|
231
|
+
fs.writeFileSync(shFile, content, { mode: 0o755 });
|
|
232
|
+
created.push(shFile);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (created.length > 0) {
|
|
237
|
+
console.log(' CLI commands registered:');
|
|
238
|
+
for (const f of created) {
|
|
239
|
+
console.log(` ${f}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ──────────────────────────────────────
|
|
245
|
+
// Helpers
|
|
246
|
+
// ──────────────────────────────────────
|
|
18
247
|
|
|
19
248
|
function ask (rl, question) {
|
|
20
249
|
return new Promise((resolve) => {
|
|
@@ -65,11 +294,34 @@ function addHook (settings, event) {
|
|
|
65
294
|
}
|
|
66
295
|
}
|
|
67
296
|
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
297
|
+
// ──────────────────────────────────────
|
|
298
|
+
// Main
|
|
299
|
+
// ──────────────────────────────────────
|
|
71
300
|
|
|
72
301
|
async function main () {
|
|
302
|
+
// 1. Register plugin in Claude Code
|
|
303
|
+
const version = getVersion();
|
|
304
|
+
|
|
305
|
+
console.log('');
|
|
306
|
+
console.log(`Registering plugin v${version} in Claude Code...`);
|
|
307
|
+
|
|
308
|
+
const installPath = copyToCache(version);
|
|
309
|
+
console.log(` Cached: ${installPath}`);
|
|
310
|
+
|
|
311
|
+
updateInstalledPlugins(version, installPath);
|
|
312
|
+
console.log(' Updated installed_plugins.json');
|
|
313
|
+
|
|
314
|
+
updateKnownMarketplaces();
|
|
315
|
+
console.log(' Updated known_marketplaces.json');
|
|
316
|
+
|
|
317
|
+
cloneOrUpdateMarketplace();
|
|
318
|
+
console.log(' Marketplace synced');
|
|
319
|
+
|
|
320
|
+
registerCli();
|
|
321
|
+
|
|
322
|
+
console.log(' Plugin registered.');
|
|
323
|
+
|
|
324
|
+
// 2. Interactive Telegram setup
|
|
73
325
|
const rl = readline.createInterface({
|
|
74
326
|
input: process.stdin,
|
|
75
327
|
output: process.stdout,
|
|
@@ -80,7 +332,6 @@ async function main () {
|
|
|
80
332
|
console.log('==================================');
|
|
81
333
|
console.log('');
|
|
82
334
|
|
|
83
|
-
// Load existing config
|
|
84
335
|
let existing = {};
|
|
85
336
|
if (fs.existsSync(configPath)) {
|
|
86
337
|
try {
|
|
@@ -93,7 +344,6 @@ async function main () {
|
|
|
93
344
|
const existingToken = existing.telegram?.token || '';
|
|
94
345
|
const existingChatId = existing.telegram?.chatId || '';
|
|
95
346
|
|
|
96
|
-
// Telegram setup
|
|
97
347
|
let token = existingToken;
|
|
98
348
|
let chatId = existingChatId;
|
|
99
349
|
|
|
@@ -103,7 +353,7 @@ async function main () {
|
|
|
103
353
|
const reuse = await ask(rl, 'Keep existing token? (Y/n): ');
|
|
104
354
|
if (reuse.toLowerCase() === 'n') {
|
|
105
355
|
token = await ask(rl, 'New Bot Token: ');
|
|
106
|
-
chatId = '';
|
|
356
|
+
chatId = '';
|
|
107
357
|
}
|
|
108
358
|
} else {
|
|
109
359
|
const useTelegram = await ask(rl, 'Configure Telegram? (y/N): ');
|
|
@@ -112,7 +362,6 @@ async function main () {
|
|
|
112
362
|
}
|
|
113
363
|
}
|
|
114
364
|
|
|
115
|
-
// Fetch chatId if we have a token but no chatId
|
|
116
365
|
if (token && !chatId) {
|
|
117
366
|
console.log('');
|
|
118
367
|
console.log('Send any message to your bot in Telegram, then press Enter.');
|
|
@@ -133,7 +382,7 @@ async function main () {
|
|
|
133
382
|
|
|
134
383
|
rl.close();
|
|
135
384
|
|
|
136
|
-
//
|
|
385
|
+
// 3. Write config
|
|
137
386
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
138
387
|
|
|
139
388
|
const platform = process.platform;
|
|
@@ -178,16 +427,13 @@ async function main () {
|
|
|
178
427
|
},
|
|
179
428
|
};
|
|
180
429
|
|
|
181
|
-
// Merge defaults with existing config (user values take priority)
|
|
182
430
|
const config = { ...defaults, ...existing };
|
|
183
|
-
// Deep-merge nested objects
|
|
184
431
|
for (const key of Object.keys(defaults)) {
|
|
185
432
|
if (defaults[key] && typeof defaults[key] === 'object' && !Array.isArray(defaults[key])) {
|
|
186
433
|
config[key] = { ...defaults[key], ...(existing[key] || {}) };
|
|
187
434
|
}
|
|
188
435
|
}
|
|
189
436
|
|
|
190
|
-
// Apply Telegram credentials from this run (if provided)
|
|
191
437
|
if (token) {
|
|
192
438
|
config.telegram.token = token;
|
|
193
439
|
}
|
|
@@ -197,7 +443,7 @@ async function main () {
|
|
|
197
443
|
|
|
198
444
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
199
445
|
|
|
200
|
-
//
|
|
446
|
+
// 4. Register hooks
|
|
201
447
|
let settings = {};
|
|
202
448
|
|
|
203
449
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -216,7 +462,7 @@ async function main () {
|
|
|
216
462
|
|
|
217
463
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
218
464
|
|
|
219
|
-
//
|
|
465
|
+
// 5. Summary
|
|
220
466
|
console.log('');
|
|
221
467
|
console.log('Installed!');
|
|
222
468
|
console.log('');
|
package/bin/uninstall.js
CHANGED
|
@@ -40,8 +40,9 @@ if (fs.existsSync(settingsPath)) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Remove config and
|
|
44
|
-
|
|
43
|
+
// Remove config, state, and resolver files
|
|
44
|
+
const resolverPath = path.join(claudeDir, 'claude-notify-resolve.js');
|
|
45
|
+
for (const file of [configPath, statePath, resolverPath]) {
|
|
45
46
|
if (fs.existsSync(file)) {
|
|
46
47
|
fs.unlinkSync(file);
|
|
47
48
|
}
|
|
@@ -397,7 +397,7 @@ If the worktree doesn't exist, it will be created automatically.
|
|
|
397
397
|
5. When Claude finishes → sends the result to Telegram
|
|
398
398
|
6. If there's a next task in the queue → starts it
|
|
399
399
|
|
|
400
|
-
---
|
|
400
|
+
---
|
|
401
401
|
|
|
402
402
|
## Projects and worktrees
|
|
403
403
|
|
|
@@ -847,7 +847,7 @@ exec(`claude -p "${userText}"`)
|
|
|
847
847
|
|
|
848
848
|
- 10 tasks in the queue per workDir (spam protection)
|
|
849
849
|
- 50 tasks total (overload protection)
|
|
850
|
-
-
|
|
850
|
+
- 30-minute timeout per task (hang protection)
|
|
851
851
|
|
|
852
852
|
---
|
|
853
853
|
|
|
@@ -863,7 +863,7 @@ claude-notify-listener status
|
|
|
863
863
|
Check:
|
|
864
864
|
|
|
865
865
|
1. Does the config exist? `cat ~/.claude/notifier.config.json`
|
|
866
|
-
2. Are `
|
|
866
|
+
2. Are `telegram.token` and `telegram.chatId` present?
|
|
867
867
|
3. Is there a `listener.projects` section?
|
|
868
868
|
4. Logs: `claude-notify-listener logs`
|
|
869
869
|
|
package/listener/listener.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// noinspection UnnecessaryLocalVariableJS
|
|
2
3
|
|
|
3
4
|
import fs from 'fs';
|
|
4
5
|
import os from 'os';
|
|
@@ -102,24 +103,35 @@ runner.on('complete', async (workDir, task, output) => {
|
|
|
102
103
|
const entry = queue.queues[workDir];
|
|
103
104
|
const label = formatLabel(entry);
|
|
104
105
|
|
|
105
|
-
//
|
|
106
|
-
|
|
106
|
+
// Delete the "Running" message
|
|
107
|
+
await poller.deleteMessage(task.runningMessageId);
|
|
108
|
+
|
|
109
|
+
// Build result: try replying to user's original message without duplicating the task text.
|
|
110
|
+
// If reply fails (user deleted their message), resend with task text included.
|
|
111
|
+
const headerShort = `✅ [${label}] Done:`;
|
|
112
|
+
const headerFull = `✅ [${label}] Done: ${escapeHtml(task.text)}`;
|
|
113
|
+
let body = '';
|
|
107
114
|
if (output) {
|
|
108
115
|
if (output.length > 20000) {
|
|
109
116
|
const head = output.slice(0, 2000);
|
|
110
117
|
const tail = output.slice(-2000);
|
|
111
|
-
|
|
112
|
-
// Also send as file
|
|
118
|
+
body = `\n\n<pre>${escapeHtml(head)}\n\n... (${output.length} chars) ...\n\n${escapeHtml(tail)}</pre>`;
|
|
113
119
|
await poller.sendDocument(
|
|
114
120
|
Buffer.from(output, 'utf-8'),
|
|
115
121
|
`result_${task.id}.txt`,
|
|
116
122
|
`Full output for: ${task.text.slice(0, 100)}`
|
|
117
123
|
);
|
|
118
124
|
} else {
|
|
119
|
-
|
|
125
|
+
body = `\n\n<pre>${escapeHtml(output)}</pre>`;
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
|
-
|
|
128
|
+
|
|
129
|
+
// Try reply to original message (short header, task text visible in quote)
|
|
130
|
+
const sentId = await poller.sendMessage(headerShort + body, task.telegramMessageId);
|
|
131
|
+
if (!sentId && task.telegramMessageId) {
|
|
132
|
+
// Reply failed — original message was deleted, send without reply but with full task text
|
|
133
|
+
await poller.sendMessage(headerFull + body);
|
|
134
|
+
}
|
|
123
135
|
|
|
124
136
|
// Process next in queue
|
|
125
137
|
const next = queue.onTaskComplete(workDir, output);
|
|
@@ -131,10 +143,14 @@ runner.on('complete', async (workDir, task, output) => {
|
|
|
131
143
|
runner.on('error', async (workDir, task, errorMsg) => {
|
|
132
144
|
const entry = queue.queues[workDir];
|
|
133
145
|
const label = formatLabel(entry);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
146
|
+
|
|
147
|
+
await poller.deleteMessage(task.runningMessageId);
|
|
148
|
+
|
|
149
|
+
const body = `\n\n<pre>${escapeHtml(errorMsg)}</pre>`;
|
|
150
|
+
const sentId = await poller.sendMessage(`❌ [${label}] Error:${body}`, task.telegramMessageId);
|
|
151
|
+
if (!sentId && task.telegramMessageId) {
|
|
152
|
+
await poller.sendMessage(`❌ [${label}] Error: ${escapeHtml(task.text)}${body}`);
|
|
153
|
+
}
|
|
138
154
|
|
|
139
155
|
const next = queue.onTaskComplete(workDir, `ERROR: ${errorMsg}`);
|
|
140
156
|
if (next) {
|
|
@@ -145,10 +161,16 @@ runner.on('error', async (workDir, task, errorMsg) => {
|
|
|
145
161
|
runner.on('timeout', async (workDir, task) => {
|
|
146
162
|
const entry = queue.queues[workDir];
|
|
147
163
|
const label = formatLabel(entry);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
const timeoutMin = Math.round(taskTimeout / 60000);
|
|
165
|
+
|
|
166
|
+
await poller.deleteMessage(task.runningMessageId);
|
|
167
|
+
|
|
168
|
+
const headerShort = `⏰ [${label}] Task forcefully stopped — timeout exceeded (${timeoutMin} min)`;
|
|
169
|
+
const headerFull = `${headerShort}: ${escapeHtml(task.text)}`;
|
|
170
|
+
const sentId = await poller.sendMessage(headerShort, task.telegramMessageId);
|
|
171
|
+
if (!sentId && task.telegramMessageId) {
|
|
172
|
+
await poller.sendMessage(headerFull);
|
|
173
|
+
}
|
|
152
174
|
|
|
153
175
|
const next = queue.onTaskComplete(workDir, 'TIMEOUT');
|
|
154
176
|
if (next) {
|
|
@@ -170,10 +192,11 @@ function formatLabel (entry) {
|
|
|
170
192
|
return `@${entry.project}`;
|
|
171
193
|
}
|
|
172
194
|
|
|
173
|
-
function startTask (workDir, task) {
|
|
195
|
+
async function startTask (workDir, task) {
|
|
174
196
|
const entry = queue.queues[workDir];
|
|
175
197
|
const label = formatLabel(entry);
|
|
176
|
-
poller.sendMessage(`⏳ [${label}] Running: ${escapeHtml(task.text)}`, task.telegramMessageId);
|
|
198
|
+
const runningMsgId = await poller.sendMessage(`⏳ [${label}] Running: ${escapeHtml(task.text)}`, task.telegramMessageId);
|
|
199
|
+
task.runningMessageId = runningMsgId;
|
|
177
200
|
try {
|
|
178
201
|
const started = runner.run(workDir, task);
|
|
179
202
|
queue.markStarted(workDir, started.pid);
|
|
@@ -50,6 +50,7 @@ export class TelegramPoller {
|
|
|
50
50
|
|
|
51
51
|
async sendMessage (text, replyToMessageId) {
|
|
52
52
|
const chunks = splitMessage(text);
|
|
53
|
+
let firstMessageId = null;
|
|
53
54
|
for (const chunk of chunks) {
|
|
54
55
|
try {
|
|
55
56
|
const body = {
|
|
@@ -68,7 +69,7 @@ export class TelegramPoller {
|
|
|
68
69
|
const data = await res.json();
|
|
69
70
|
if (!data.ok) {
|
|
70
71
|
// Retry without HTML parse mode
|
|
71
|
-
await fetch(`${this.baseUrl}/sendMessage`, {
|
|
72
|
+
const res2 = await fetch(`${this.baseUrl}/sendMessage`, {
|
|
72
73
|
method: 'POST',
|
|
73
74
|
headers: { 'Content-Type': 'application/json' },
|
|
74
75
|
body: JSON.stringify({
|
|
@@ -77,11 +78,36 @@ export class TelegramPoller {
|
|
|
77
78
|
...(replyToMessageId ? { reply_to_message_id: replyToMessageId } : {}),
|
|
78
79
|
}),
|
|
79
80
|
});
|
|
81
|
+
const data2 = await res2.json();
|
|
82
|
+
if (data2.ok && !firstMessageId) {
|
|
83
|
+
firstMessageId = data2.result.message_id;
|
|
84
|
+
}
|
|
85
|
+
} else if (!firstMessageId) {
|
|
86
|
+
firstMessageId = data.result.message_id;
|
|
80
87
|
}
|
|
81
88
|
} catch (err) {
|
|
82
89
|
this.logger.error(`sendMessage error: ${err.message}`);
|
|
83
90
|
}
|
|
84
91
|
}
|
|
92
|
+
return firstMessageId;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async deleteMessage (messageId) {
|
|
96
|
+
if (!messageId) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await fetch(`${this.baseUrl}/deleteMessage`, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
chat_id: this.chatId,
|
|
105
|
+
message_id: messageId,
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
this.logger.error(`deleteMessage error: ${err.message}`);
|
|
110
|
+
}
|
|
85
111
|
}
|
|
86
112
|
|
|
87
113
|
async sendDocument (buffer, filename, caption) {
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "claude-notification-plugin",
|
|
3
|
-
"productName": "claude-notification-plugin",
|
|
4
|
-
"version": "1.0.
|
|
5
|
-
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"engines": {
|
|
8
|
-
"node": ">=18.0.0"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
".claude-plugin/",
|
|
12
|
-
"bin/",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"claude-notify-
|
|
22
|
-
"claude-
|
|
23
|
-
"claude-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"lint": "eslint .",
|
|
29
|
-
"lint:fix": "eslint --fix ."
|
|
30
|
-
},
|
|
31
|
-
"keywords": [
|
|
32
|
-
"claude",
|
|
33
|
-
"claude-code",
|
|
34
|
-
"notifications",
|
|
35
|
-
"telegram",
|
|
36
|
-
"hooks",
|
|
37
|
-
"macos",
|
|
38
|
-
"linux",
|
|
39
|
-
"cross-platform"
|
|
40
|
-
],
|
|
41
|
-
"author": {
|
|
42
|
-
"name": "Viacheslav Makarov",
|
|
43
|
-
"email": "npmjs@bazilio.ru"
|
|
44
|
-
},
|
|
45
|
-
"license": "MIT",
|
|
46
|
-
"repository": {
|
|
47
|
-
"type": "git",
|
|
48
|
-
"url": "git+https://github.com/Bazilio-san/claude-notification-plugin.git"
|
|
49
|
-
},
|
|
50
|
-
"homepage": "https://github.com/Bazilio-san/claude-notification-plugin#readme",
|
|
51
|
-
"publishConfig": {
|
|
52
|
-
"access": "public"
|
|
53
|
-
},
|
|
54
|
-
"dependencies": {
|
|
55
|
-
"node-notifier": "^10.0.1"
|
|
56
|
-
},
|
|
57
|
-
"devDependencies": {
|
|
58
|
-
"eslint-plugin-import": "^2.31.0"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-notification-plugin",
|
|
3
|
+
"productName": "claude-notification-plugin",
|
|
4
|
+
"version": "1.0.88",
|
|
5
|
+
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18.0.0"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
".claude-plugin/",
|
|
12
|
+
"bin/",
|
|
13
|
+
"hooks/",
|
|
14
|
+
"listener/",
|
|
15
|
+
"notifier/",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"claude-notify-install": "bin/install.js",
|
|
21
|
+
"claude-notify-uninstall": "bin/uninstall.js",
|
|
22
|
+
"claude-notifier": "notifier/notifier.js",
|
|
23
|
+
"claude-notify-listener": "bin/listener-cli.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"preuninstall": "node bin/uninstall.js",
|
|
27
|
+
"postinstall": "node bin/install.js",
|
|
28
|
+
"lint": "eslint .",
|
|
29
|
+
"lint:fix": "eslint --fix ."
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"claude",
|
|
33
|
+
"claude-code",
|
|
34
|
+
"notifications",
|
|
35
|
+
"telegram",
|
|
36
|
+
"hooks",
|
|
37
|
+
"macos",
|
|
38
|
+
"linux",
|
|
39
|
+
"cross-platform"
|
|
40
|
+
],
|
|
41
|
+
"author": {
|
|
42
|
+
"name": "Viacheslav Makarov",
|
|
43
|
+
"email": "npmjs@bazilio.ru"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/Bazilio-san/claude-notification-plugin.git"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/Bazilio-san/claude-notification-plugin#readme",
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"node-notifier": "^10.0.1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"eslint-plugin-import": "^2.31.0"
|
|
59
|
+
}
|
|
60
|
+
}
|