claude-notification-plugin 1.0.112 → 1.0.115
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/bin/install.js +20 -24
- package/bin/listener-cli.js +188 -22
- package/commit-sha +1 -1
- package/listener/listener.js +31 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.115",
|
|
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/bin/install.js
CHANGED
|
@@ -107,10 +107,14 @@ function stopListenerIfRunning () {
|
|
|
107
107
|
let stopped = false;
|
|
108
108
|
const pidFromFile = (() => {
|
|
109
109
|
try {
|
|
110
|
-
if (!fs.existsSync(pidFile))
|
|
110
|
+
if (!fs.existsSync(pidFile)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
111
113
|
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
112
114
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
113
|
-
} catch {
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
114
118
|
})();
|
|
115
119
|
|
|
116
120
|
if (pidFromFile && killProcessTree(pidFromFile)) {
|
|
@@ -398,24 +402,15 @@ async function fetchChatId (token) {
|
|
|
398
402
|
return null;
|
|
399
403
|
}
|
|
400
404
|
|
|
401
|
-
function
|
|
405
|
+
function removeHook (settings, event) {
|
|
402
406
|
if (!settings.hooks[event]) {
|
|
403
|
-
|
|
407
|
+
return;
|
|
404
408
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
matcher.hooks?.some((h) => h.command?.includes(HOOK_COMMAND)),
|
|
409
|
+
settings.hooks[event] = settings.hooks[event].filter((matcher) =>
|
|
410
|
+
!matcher.hooks?.some((h) => h.command?.includes(HOOK_COMMAND)),
|
|
408
411
|
);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
settings.hooks[event].push({
|
|
412
|
-
hooks: [
|
|
413
|
-
{
|
|
414
|
-
type: 'command',
|
|
415
|
-
command: HOOK_COMMAND,
|
|
416
|
-
},
|
|
417
|
-
],
|
|
418
|
-
});
|
|
412
|
+
if (settings.hooks[event].length === 0) {
|
|
413
|
+
delete settings.hooks[event];
|
|
419
414
|
}
|
|
420
415
|
}
|
|
421
416
|
|
|
@@ -595,16 +590,17 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
595
590
|
}
|
|
596
591
|
}
|
|
597
592
|
|
|
598
|
-
settings.hooks = settings.hooks || {};
|
|
599
|
-
|
|
600
|
-
addHook(settings, 'UserPromptSubmit');
|
|
601
|
-
addHook(settings, 'Stop');
|
|
602
|
-
addHook(settings, 'Notification');
|
|
603
|
-
|
|
604
593
|
// Register plugin as enabled
|
|
605
594
|
settings.enabledPlugins = settings.enabledPlugins || {};
|
|
606
595
|
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
607
596
|
|
|
597
|
+
// When the plugin is enabled, Claude Code loads hooks from hooks/hooks.json automatically.
|
|
598
|
+
// Remove any duplicate hooks from settings.json to avoid double notifications.
|
|
599
|
+
settings.hooks = settings.hooks || {};
|
|
600
|
+
removeHook(settings, 'UserPromptSubmit');
|
|
601
|
+
removeHook(settings, 'Stop');
|
|
602
|
+
removeHook(settings, 'Notification');
|
|
603
|
+
|
|
608
604
|
// Register marketplace
|
|
609
605
|
settings.extraKnownMarketplaces = settings.extraKnownMarketplaces || {};
|
|
610
606
|
settings.extraKnownMarketplaces[MARKETPLACE_KEY] = {
|
|
@@ -635,7 +631,7 @@ Send any message to your bot in Telegram, then press Enter.\x1b[0m`);
|
|
|
635
631
|
console.log(`
|
|
636
632
|
Installed!
|
|
637
633
|
${listenerLine}
|
|
638
|
-
|
|
634
|
+
Plugin hooks (via hooks/hooks.json):
|
|
639
635
|
- UserPromptSubmit (start timer)
|
|
640
636
|
- Stop (task finished)
|
|
641
637
|
- Notification (waiting for input)
|
package/bin/listener-cli.js
CHANGED
|
@@ -278,6 +278,91 @@ function ask (rl, question) {
|
|
|
278
278
|
});
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
function askYesNo (rl, question) {
|
|
282
|
+
return new Promise((resolve) => {
|
|
283
|
+
rl.question(question, (answer) => {
|
|
284
|
+
const a = answer.trim().toLowerCase();
|
|
285
|
+
resolve(a === 'y' || a === 'yes');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isValidPath (p) {
|
|
291
|
+
if (!p || p.includes('\0')) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
path.resolve(p);
|
|
296
|
+
return true;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function ensureDir (dirPath) {
|
|
303
|
+
try {
|
|
304
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
305
|
+
return fs.existsSync(dirPath);
|
|
306
|
+
} catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function isValidAlias (alias) {
|
|
312
|
+
return /^[a-zA-Z0-9_-]+$/.test(alias);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parsePositiveInt (str, fallback) {
|
|
316
|
+
if (!str) {
|
|
317
|
+
return fallback;
|
|
318
|
+
}
|
|
319
|
+
const n = parseInt(str, 10);
|
|
320
|
+
if (isNaN(n) || n <= 0) {
|
|
321
|
+
console.log(` \u26a0 Invalid value "${str}". Using default: ${fallback}`);
|
|
322
|
+
return fallback;
|
|
323
|
+
}
|
|
324
|
+
return n;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function validateDir (inputPath, defaultPath) {
|
|
328
|
+
const chosen = inputPath || defaultPath;
|
|
329
|
+
if (!isValidPath(chosen)) {
|
|
330
|
+
console.log(` \u26a0 Invalid path "${chosen}". Using default: ${defaultPath}`);
|
|
331
|
+
if (!ensureDir(defaultPath)) {
|
|
332
|
+
console.log(` \u26a0 Cannot create "${defaultPath}". Please check permissions.`);
|
|
333
|
+
}
|
|
334
|
+
return defaultPath;
|
|
335
|
+
}
|
|
336
|
+
if (!ensureDir(chosen)) {
|
|
337
|
+
console.log(` \u26a0 Cannot create "${chosen}". Using default: ${defaultPath}`);
|
|
338
|
+
ensureDir(defaultPath);
|
|
339
|
+
return defaultPath;
|
|
340
|
+
}
|
|
341
|
+
return chosen;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function validateProjectPath (rl, inputPath) {
|
|
345
|
+
if (!inputPath) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
if (!isValidPath(inputPath)) {
|
|
349
|
+
console.log(' \u26a0 Invalid path. Project will not be set.');
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const resolved = path.resolve(inputPath);
|
|
353
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
354
|
+
return resolved;
|
|
355
|
+
}
|
|
356
|
+
const create = await askYesNo(rl, ` Directory "${resolved}" does not exist. Create it? (y/n): `);
|
|
357
|
+
if (create) {
|
|
358
|
+
if (ensureDir(resolved)) {
|
|
359
|
+
return resolved;
|
|
360
|
+
}
|
|
361
|
+
console.log(` \u26a0 Cannot create "${resolved}".`);
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
281
366
|
async function setupListener () {
|
|
282
367
|
let config = {};
|
|
283
368
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
@@ -313,33 +398,114 @@ Listener Setup
|
|
|
313
398
|
Press Enter to keep current value shown in [brackets].
|
|
314
399
|
`);
|
|
315
400
|
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
const maxTotalStr = await ask(rl, `Max total tasks [${defaults.maxTotalTasks}]: `) || String(defaults.maxTotalTasks);
|
|
320
|
-
const logDir = await ask(rl, `Log dir [${defaults.logDir}]: `) || defaults.logDir;
|
|
321
|
-
const taskLogDir = await ask(rl, `Task log dir [${defaults.taskLogDir}]: `) || defaults.taskLogDir;
|
|
322
|
-
const projectPath = await ask(rl, `Default project path [${defaults.projectPath || '(none)'}]: `) || defaults.projectPath;
|
|
401
|
+
// --- Directories ---
|
|
402
|
+
const worktreeInput = await ask(rl, `Worktree base dir [${defaults.worktreeBaseDir}]: `);
|
|
403
|
+
L.worktreeBaseDir = validateDir(worktreeInput, defaults.worktreeBaseDir);
|
|
323
404
|
|
|
324
|
-
rl.
|
|
405
|
+
const logDirInput = await ask(rl, `Log dir [${defaults.logDir}]: `);
|
|
406
|
+
L.logDir = validateDir(logDirInput, defaults.logDir);
|
|
407
|
+
|
|
408
|
+
const taskLogDirInput = await ask(rl, `Task log dir [${defaults.taskLogDir}]: `);
|
|
409
|
+
L.taskLogDir = validateDir(taskLogDirInput, defaults.taskLogDir);
|
|
410
|
+
|
|
411
|
+
// --- Numeric params ---
|
|
412
|
+
const taskTimeoutStr = await ask(rl, `Task timeout, minutes [${defaults.taskTimeoutMinutes}]: `);
|
|
413
|
+
L.taskTimeoutMinutes = parsePositiveInt(taskTimeoutStr, defaults.taskTimeoutMinutes);
|
|
414
|
+
|
|
415
|
+
const maxQueueStr = await ask(rl, `Max queue per work dir [${defaults.maxQueuePerWorkDir}]: `);
|
|
416
|
+
L.maxQueuePerWorkDir = parsePositiveInt(maxQueueStr, defaults.maxQueuePerWorkDir);
|
|
417
|
+
|
|
418
|
+
const maxTotalStr = await ask(rl, `Max total tasks [${defaults.maxTotalTasks}]: `);
|
|
419
|
+
L.maxTotalTasks = parsePositiveInt(maxTotalStr, defaults.maxTotalTasks);
|
|
325
420
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
L.logDir = logDir;
|
|
331
|
-
L.taskLogDir = taskLogDir;
|
|
421
|
+
// --- Default project ---
|
|
422
|
+
console.log('');
|
|
423
|
+
const projectInput = await ask(rl, `Default project path [${defaults.projectPath || '(none)'}]: `);
|
|
424
|
+
const rawProjectPath = projectInput || defaults.projectPath;
|
|
332
425
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
426
|
+
L.projects = L.projects || {};
|
|
427
|
+
let hasValidProject = false;
|
|
428
|
+
|
|
429
|
+
if (rawProjectPath) {
|
|
430
|
+
const validatedPath = await validateProjectPath(rl, rawProjectPath);
|
|
431
|
+
if (validatedPath) {
|
|
432
|
+
L.projects.default = L.projects.default || {};
|
|
433
|
+
L.projects.default.path = validatedPath;
|
|
434
|
+
hasValidProject = true;
|
|
435
|
+
} else {
|
|
436
|
+
delete L.projects.default;
|
|
437
|
+
console.log(' \u26a0 Default project will not be set. Listener will not start without at least one project.');
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
delete L.projects.default;
|
|
441
|
+
console.log(' \u26a0 No default project configured. Listener will not start without at least one project.');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// --- Additional projects loop ---
|
|
445
|
+
// Count existing non-default projects
|
|
446
|
+
const existingAliases = Object.keys(L.projects).filter(a => a !== 'default');
|
|
447
|
+
if (existingAliases.length > 0) {
|
|
448
|
+
console.log(`\nExisting projects: ${existingAliases.join(', ')}`);
|
|
337
449
|
}
|
|
338
450
|
|
|
451
|
+
while (true) {
|
|
452
|
+
console.log('');
|
|
453
|
+
const addMore = await askYesNo(rl, 'Add another project? (y/n): ');
|
|
454
|
+
if (!addMore) {
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Ask alias with validation loop
|
|
459
|
+
let alias = '';
|
|
460
|
+
while (true) {
|
|
461
|
+
alias = await ask(rl, 'Project alias: ');
|
|
462
|
+
if (!alias) {
|
|
463
|
+
console.log(' \u26a0 Alias cannot be empty.');
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (alias === 'default') {
|
|
467
|
+
console.log(' \u26a0 "default" is reserved. Choose a different name.');
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (!isValidAlias(alias)) {
|
|
471
|
+
console.log(' \u26a0 Invalid alias. Allowed characters: a-z, A-Z, 0-9, -, _');
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (L.projects[alias]) {
|
|
475
|
+
console.log(` \u26a0 Alias "${alias}" already exists. Choose a different name.`);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Ask path with validation
|
|
482
|
+
const projPathInput = await ask(rl, `Project path for "${alias}": `);
|
|
483
|
+
if (!projPathInput) {
|
|
484
|
+
console.log(` \u26a0 Project "${alias}" was not added (no path provided).`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const validatedProjPath = await validateProjectPath(rl, projPathInput);
|
|
489
|
+
if (validatedProjPath) {
|
|
490
|
+
L.projects[alias] = { path: validatedProjPath };
|
|
491
|
+
hasValidProject = true;
|
|
492
|
+
console.log(` \u2713 Project "${alias}" added: ${validatedProjPath}`);
|
|
493
|
+
} else {
|
|
494
|
+
console.log(` \u26a0 Project "${alias}" was not added (invalid path).`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
rl.close();
|
|
499
|
+
|
|
339
500
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
340
501
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
502
|
+
const projectCount = Object.keys(L.projects).length;
|
|
503
|
+
console.log(`\nListener config saved to ${CONFIG_FILE}`);
|
|
504
|
+
if (hasValidProject || projectCount > 0) {
|
|
505
|
+
console.log('Run "claude-notify listener start" to apply.');
|
|
506
|
+
} else {
|
|
507
|
+
console.log('\u26a0 No projects configured. Listener will not start until at least one project is added.');
|
|
508
|
+
console.log('Run "claude-notify listener setup" again to add projects.');
|
|
509
|
+
}
|
|
510
|
+
console.log('');
|
|
345
511
|
}
|
package/commit-sha
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
02ac0d7e44dac4ff0bb58a912fa8c92825f90cf4
|
package/listener/listener.js
CHANGED
|
@@ -51,10 +51,40 @@ if (!token || !chatId) {
|
|
|
51
51
|
|
|
52
52
|
if (!config.listener?.projects || Object.keys(config.listener.projects).length === 0) {
|
|
53
53
|
logger.error('No projects defined in config.listener.projects');
|
|
54
|
-
console.error('No projects defined in config.listener.projects');
|
|
54
|
+
console.error('No projects defined in config.listener.projects. Run "claude-notify listener setup" to configure.');
|
|
55
55
|
process.exit(1);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Validate project paths — skip projects with missing/invalid directories
|
|
59
|
+
const validatedProjects = {};
|
|
60
|
+
for (const [alias, proj] of Object.entries(config.listener.projects)) {
|
|
61
|
+
const projPath = typeof proj === 'string' ? proj : proj?.path;
|
|
62
|
+
if (!projPath) {
|
|
63
|
+
logger.warn(`Project "${alias}": no path configured, skipping`);
|
|
64
|
+
console.error(`\u26a0 Project "${alias}": no path configured, skipping`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const stat = fs.statSync(projPath);
|
|
69
|
+
if (!stat.isDirectory()) {
|
|
70
|
+
logger.warn(`Project "${alias}": path "${projPath}" is not a directory, skipping`);
|
|
71
|
+
console.error(`\u26a0 Project "${alias}": path "${projPath}" is not a directory, skipping`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
validatedProjects[alias] = proj;
|
|
75
|
+
} catch {
|
|
76
|
+
logger.warn(`Project "${alias}": path "${projPath}" does not exist, skipping`);
|
|
77
|
+
console.error(`\u26a0 Project "${alias}": path "${projPath}" does not exist, skipping`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Object.keys(validatedProjects).length === 0) {
|
|
82
|
+
logger.error('No projects with valid paths found in config.listener.projects');
|
|
83
|
+
console.error('No projects with valid paths found. Run "claude-notify listener setup" to configure.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
config.listener.projects = validatedProjects;
|
|
58
88
|
const listenerConfig = config.listener;
|
|
59
89
|
const taskTimeoutMinutes = listenerConfig.taskTimeoutMinutes || 30;
|
|
60
90
|
const taskTimeout = taskTimeoutMinutes * 60_000;
|
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.115",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|