claude-notification-plugin 1.1.74 → 1.1.76

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-notification-plugin",
3
- "version": "1.1.74",
3
+ "version": "1.1.76",
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/constants.js CHANGED
@@ -1,3 +1,4 @@
1
+ import fs from 'fs';
1
2
  import os from 'os';
2
3
  import path from 'path';
3
4
 
@@ -39,6 +40,31 @@ export const SHORTCUT_DIR = path.join(
39
40
  export const SHORTCUT_PATH = path.join(SHORTCUT_DIR, SHORTCUT_NAME);
40
41
  export const APP_ID = 'Claude Notify';
41
42
 
43
+ /**
44
+ * Find the alias of the project marked as default (isDefault: true).
45
+ * Falls back to the first project key if none is marked.
46
+ */
47
+ export function getDefaultProject (projects) {
48
+ if (!projects || typeof projects !== 'object') {
49
+ return null;
50
+ }
51
+ for (const [alias, proj] of Object.entries(projects)) {
52
+ if (typeof proj === 'object' && proj.isDefault) {
53
+ return alias;
54
+ }
55
+ }
56
+ // Fallback: first project
57
+ const keys = Object.keys(projects);
58
+ return keys.length > 0 ? keys[0] : null;
59
+ }
60
+
61
+ /**
62
+ * Save config object back to CONFIG_PATH.
63
+ */
64
+ export function saveConfig (config) {
65
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
66
+ }
67
+
42
68
  // Plugin identity
43
69
  export const HOOK_COMMAND = 'claude-notify';
44
70
  export const MARKETPLACE_KEY = 'bazilio-plugins';
@@ -6,7 +6,7 @@ import readline from 'readline';
6
6
  import { spawn, execSync } from 'child_process';
7
7
  import { fileURLToPath } from 'url';
8
8
  import {
9
- HOME, CLAUDE_DIR, CONFIG_PATH, PID_PATH, LISTENER_LOG_FILENAME,
9
+ HOME, CLAUDE_DIR, CONFIG_PATH, PID_PATH, LISTENER_LOG_FILENAME, getDefaultProject,
10
10
  } from './constants.js';
11
11
 
12
12
  const __filename = fileURLToPath(import.meta.url);
@@ -116,7 +116,7 @@ async function startDaemon () {
116
116
  console.error(JSON.stringify({
117
117
  listener: {
118
118
  projects: {
119
- default: { path: '/path/to/your/project' },
119
+ myproject: { path: '/path/to/your/project', isDefault: true },
120
120
  },
121
121
  },
122
122
  }, null, 2));
@@ -453,7 +453,10 @@ async function setupListener () {
453
453
  maxTotalTasks: L.maxTotalTasks ?? 50,
454
454
  logDir: L.logDir || path.join(HOME, '.claude'),
455
455
  taskLogDir: L.taskLogDir || path.join(HOME, '.claude'),
456
- projectPath: L.projects?.default?.path || '',
456
+ projectPath: (() => {
457
+ const defAlias = getDefaultProject(L.projects);
458
+ return defAlias ? (L.projects[defAlias]?.path || '') : '';
459
+ })(),
457
460
  };
458
461
 
459
462
  const rl = readline.createInterface({
@@ -520,30 +523,41 @@ Permission mode for claude -p (tools access):
520
523
 
521
524
  // --- Default project ---
522
525
  console.log('');
523
- const projectInput = await ask(rl, `Default project path [${defaults.projectPath || '(none)'}]: `);
524
- const rawProjectPath = projectInput || defaults.projectPath;
525
-
526
526
  L.projects = L.projects || {};
527
+ const currentDefaultAlias = getDefaultProject(L.projects);
528
+ const currentDefaultPath = currentDefaultAlias ? (L.projects[currentDefaultAlias]?.path || '') : '';
529
+
530
+ const projectInput = await ask(rl, `Default project path [${currentDefaultPath || '(none)'}]: `);
531
+ const rawProjectPath = projectInput || currentDefaultPath;
527
532
  let hasValidProject = false;
528
533
 
529
534
  if (rawProjectPath) {
530
535
  const validatedPath = await validateProjectPath(rl, rawProjectPath);
531
536
  if (validatedPath) {
532
- L.projects.default = L.projects.default || {};
533
- L.projects.default.path = validatedPath;
537
+ let aliasForDefault = currentDefaultAlias;
538
+ if (!aliasForDefault) {
539
+ const aliasInput = await ask(rl, 'Alias for default project: ');
540
+ aliasForDefault = aliasInput && isValidAlias(aliasInput) ? aliasInput : 'main';
541
+ }
542
+ L.projects[aliasForDefault] = L.projects[aliasForDefault] || {};
543
+ L.projects[aliasForDefault].path = validatedPath;
544
+ // Set isDefault on this project, clear from others
545
+ for (const proj of Object.values(L.projects)) {
546
+ if (typeof proj === 'object') {
547
+ delete proj.isDefault;
548
+ }
549
+ }
550
+ L.projects[aliasForDefault].isDefault = true;
534
551
  hasValidProject = true;
535
552
  } else {
536
- delete L.projects.default;
537
553
  console.log(' \u26a0 Default project will not be set. Listener will not start without at least one project.');
538
554
  }
539
555
  } else {
540
- delete L.projects.default;
541
556
  console.log(' \u26a0 No default project configured. Listener will not start without at least one project.');
542
557
  }
543
558
 
544
559
  // --- Additional projects loop ---
545
- // Count existing non-default projects
546
- const existingAliases = Object.keys(L.projects).filter(a => a !== 'default');
560
+ const existingAliases = Object.keys(L.projects);
547
561
  if (existingAliases.length > 0) {
548
562
  console.log(`\nExisting projects: ${existingAliases.join(', ')}`);
549
563
  }
@@ -563,10 +577,6 @@ Permission mode for claude -p (tools access):
563
577
  console.log(' \u26a0 Alias cannot be empty.');
564
578
  continue;
565
579
  }
566
- if (alias === 'default') {
567
- console.log(' \u26a0 "default" is reserved. Choose a different name.');
568
- continue;
569
- }
570
580
  if (!isValidAlias(alias)) {
571
581
  console.log(' \u26a0 Invalid alias. Allowed characters: a-z, A-Z, 0-9, -, _');
572
582
  continue;
package/commit-sha CHANGED
@@ -1 +1 @@
1
- 40748d991517178165dc7d6f9c3a71b8b1ee7669
1
+ 8d2fc918805c6a42eb0158fbc60e35c34761ed82
@@ -229,15 +229,27 @@ function formatToolUse (tool) {
229
229
  : (typeof input.glob === 'string' ? input.glob : '');
230
230
 
231
231
  const flags = [];
232
- if (input['-n']) flags.push('-n');
233
- if (input['-C']) flags.push(`-C ${input['-C']}`);
232
+ if (input['-n']) {
233
+ flags.push('-n');
234
+ }
235
+ if (input['-C']) {
236
+ flags.push(`-C ${input['-C']}`);
237
+ }
234
238
  if (!input['-C'] && (typeof input.context === 'number' || typeof input.context === 'string')) {
235
239
  flags.push(`-C ${input.context}`);
236
240
  }
237
- if (input['-i']) flags.push('-i');
238
- if (input['-A']) flags.push(`-A ${input['-A']}`);
239
- if (input['-B']) flags.push(`-B ${input['-B']}`);
240
- if (input.head_limit) flags.push(`head ${input.head_limit}`);
241
+ if (input['-i']) {
242
+ flags.push('-i');
243
+ }
244
+ if (input['-A']) {
245
+ flags.push(`-A ${input['-A']}`);
246
+ }
247
+ if (input['-B']) {
248
+ flags.push(`-B ${input['-B']}`);
249
+ }
250
+ if (input.head_limit) {
251
+ flags.push(`head ${input.head_limit}`);
252
+ }
241
253
  const flagStr = flags.length ? ` ${flags.join(' ')}` : '';
242
254
 
243
255
  return where
@@ -11,7 +11,7 @@ import { WorkQueue } from './work-queue.js';
11
11
  import { PtyRunner } from './pty-runner.js';
12
12
  import { WorktreeManager } from './worktree-manager.js';
13
13
  import { parseMessage, parseTarget } from './message-parser.js';
14
- import { CLAUDE_DIR, CONFIG_PATH, LISTENER_LOG_FILENAME } from '../bin/constants.js';
14
+ import { CLAUDE_DIR, CONFIG_PATH, LISTENER_LOG_FILENAME, getDefaultProject, saveConfig } from '../bin/constants.js';
15
15
  import { JsonlReader, resolveJsonlPath, resolveJsonlByMtime } from './jsonl-reader.js';
16
16
 
17
17
  // ----------------------
@@ -514,6 +514,8 @@ async function handleCommand (cmd, args) {
514
514
  return handleNewSession(args);
515
515
  case '/projects':
516
516
  return handleProjects();
517
+ case '/setdefault':
518
+ return handleSetDefault(args);
517
519
  case '/worktrees':
518
520
  return handleWorktrees(args);
519
521
  case '/worktree':
@@ -652,7 +654,7 @@ function handleQueue () {
652
654
 
653
655
  async function handleCancel (args) {
654
656
  const target = parseTarget(args);
655
- const projectAlias = target?.project || 'default';
657
+ const projectAlias = target?.project || getDefaultProject(listenerConfig.projects);
656
658
  const branch = target?.branch || null;
657
659
 
658
660
  let workDir;
@@ -703,7 +705,7 @@ function handleDrop (args) {
703
705
 
704
706
  function handleClear (args) {
705
707
  const target = parseTarget(args);
706
- const projectAlias = target?.project || 'default';
708
+ const projectAlias = target?.project || getDefaultProject(listenerConfig.projects);
707
709
  const branch = target?.branch || null;
708
710
 
709
711
  let workDir;
@@ -726,7 +728,7 @@ function handleClear (args) {
726
728
 
727
729
  function handleNewSession (args) {
728
730
  const target = parseTarget(args);
729
- const projectAlias = target?.project || 'default';
731
+ const projectAlias = target?.project || getDefaultProject(listenerConfig.projects);
730
732
  const branch = target?.branch || null;
731
733
 
732
734
  let workDir;
@@ -751,10 +753,12 @@ function handleNewSession (args) {
751
753
 
752
754
  function handleProjects () {
753
755
  const projects = listenerConfig.projects;
756
+ const defaultAlias = getDefaultProject(projects);
754
757
  let text = '📂 <b>Projects:</b>\n';
755
758
  for (const [alias, proj] of Object.entries(projects)) {
756
759
  const projPath = typeof proj === 'string' ? proj : proj.path;
757
- text += `\n<b>&${escapeHtml(alias)}</b> <code>${escapeHtml(projPath)}</code>`;
760
+ const icon = alias === defaultAlias ? '🏠 ' : '';
761
+ text += `\n${icon}<b>&${escapeHtml(alias)}</b> → <code>${escapeHtml(projPath)}</code>`;
758
762
  const worktrees = typeof proj === 'object' ? proj.worktrees : null;
759
763
  if (worktrees && Object.keys(worktrees).length > 0) {
760
764
  for (const [branch, wtPath] of Object.entries(worktrees)) {
@@ -762,7 +766,63 @@ function handleProjects () {
762
766
  }
763
767
  }
764
768
  }
765
- return text;
769
+
770
+ const buttons = [];
771
+ // "Set Default" button
772
+ buttons.push([{ text: '🏠 Set Default', callback_data: '/setdefault' }]);
773
+
774
+ return { text, replyMarkup: { inline_keyboard: buttons } };
775
+ }
776
+
777
+ function handleSetDefault (args) {
778
+ const projects = listenerConfig.projects;
779
+
780
+ // No args — show inline keyboard with project list
781
+ if (!args || !args.trim()) {
782
+ const defaultAlias = getDefaultProject(projects);
783
+ const buttons = [];
784
+ for (const [alias, proj] of Object.entries(projects)) {
785
+ const projPath = typeof proj === 'string' ? proj : proj.path;
786
+ const icon = alias === defaultAlias ? '🏠 ' : '';
787
+ buttons.push([{
788
+ text: `${icon}${alias} — ${projPath}`,
789
+ callback_data: `/setdefault ${alias}`,
790
+ }]);
791
+ }
792
+ return {
793
+ text: '🏠 <b>Select default project:</b>',
794
+ replyMarkup: { inline_keyboard: buttons },
795
+ };
796
+ }
797
+
798
+ // Args provided — set the default
799
+ const alias = args.trim();
800
+ if (!projects[alias]) {
801
+ return `❌ Project "<b>${escapeHtml(alias)}</b>" not found. Use /projects to list.`;
802
+ }
803
+
804
+ // Clear isDefault from all projects, set on chosen
805
+ for (const proj of Object.values(projects)) {
806
+ if (typeof proj === 'object') {
807
+ delete proj.isDefault;
808
+ }
809
+ }
810
+ const proj = projects[alias];
811
+ if (typeof proj === 'object') {
812
+ proj.isDefault = true;
813
+ }
814
+
815
+ // Persist to config file
816
+ try {
817
+ saveConfig(config);
818
+ logger.info(`Default project changed to "${alias}"`);
819
+ } catch (err) {
820
+ logger.error(`Failed to save config: ${err.message}`);
821
+ return `❌ Failed to save config: ${escapeHtml(err.message)}`;
822
+ }
823
+
824
+ const projPath = typeof proj === 'string' ? proj : proj.path;
825
+ return `✅ Default project: <b>&${escapeHtml(alias)}</b> → <code>${escapeHtml(projPath)}</code>`;
766
826
  }
767
827
 
768
828
  function handleWorktrees (args) {
@@ -931,6 +991,9 @@ const MENU_KEYBOARD = {
931
991
  [
932
992
  { text: '📜 History', callback_data: '/history' },
933
993
  { text: '🖥 PTY', callback_data: '/pty' },
994
+ { text: '🏠 Default', callback_data: '/setdefault' },
995
+ ],
996
+ [
934
997
  { text: '📖 Help', callback_data: '/help' },
935
998
  ],
936
999
  ],
@@ -948,6 +1011,7 @@ function handleHelp () {
948
1011
  /clear &project[/branch] — clear queue + reset session
949
1012
  /newsession [&project[/branch]] — reset session (keep queue)
950
1013
  /projects — list projects
1014
+ /setdefault — change default project
951
1015
  /worktrees &project — project worktrees
952
1016
  /worktree &project/branch — create worktree
953
1017
  /rmworktree &project/branch — remove worktree
@@ -1065,7 +1129,7 @@ async function mainLoop () {
1065
1129
  await poller.answerCallbackQuery(msg.callbackQueryId);
1066
1130
  }
1067
1131
 
1068
- const parsed = parseMessage(msg.text);
1132
+ const parsed = parseMessage(msg.text, getDefaultProject(listenerConfig.projects));
1069
1133
  if (!parsed) {
1070
1134
  continue;
1071
1135
  }
@@ -1099,6 +1163,7 @@ async function mainLoop () {
1099
1163
  { command: 'status', description: 'Status of all projects' },
1100
1164
  { command: 'queue', description: 'Show all queues' },
1101
1165
  { command: 'projects', description: 'List projects' },
1166
+ { command: 'setdefault', description: 'Change default project' },
1102
1167
  { command: 'history', description: 'Recent task history' },
1103
1168
  { command: 'pty', description: 'PTY session diagnostics' },
1104
1169
  { command: 'help', description: 'Show all commands' },
@@ -7,12 +7,15 @@
7
7
  * /command args → { type: 'command', cmd, args }
8
8
  * &project/branch text → { type: 'task', project, branch, text }
9
9
  * &project text → { type: 'task', project, branch: null, text }
10
- * text → { type: 'task', project: 'default', branch: null, text }
10
+ * text → { type: 'task', project: <defaultProject>, branch: null, text }
11
11
  *
12
12
  * Any /word is treated as a command (known or unknown).
13
13
  * Project designation uses & prefix: &project or &project/branch.
14
+ *
15
+ * @param {string} text - The message text.
16
+ * @param {string} [defaultProject] - Alias of the default project (used for plain text tasks).
14
17
  */
15
- export function parseMessage (text) {
18
+ export function parseMessage (text, defaultProject) {
16
19
  if (!text || typeof text !== 'string') {
17
20
  return null;
18
21
  }
@@ -61,7 +64,7 @@ export function parseMessage (text) {
61
64
  // Plain text → default project
62
65
  return {
63
66
  type: 'task',
64
- project: 'default',
67
+ project: defaultProject || 'default',
65
68
  branch: null,
66
69
  text: trimmed,
67
70
  };
@@ -26,6 +26,28 @@ function debugLog (config, ...args) {
26
26
  }
27
27
  }
28
28
 
29
+ function normalizePath (p) {
30
+ return path.resolve(p).replace(/\\/g, '/').toLowerCase();
31
+ }
32
+
33
+ function resolveProjectName (cwd, config) {
34
+ const fallback = path.basename(cwd);
35
+ const projects = config?.listenerProjects;
36
+ if (!projects || typeof projects !== 'object') {
37
+ return fallback;
38
+ }
39
+ const normalizedCwd = normalizePath(cwd);
40
+ for (const entry of Object.values(projects)) {
41
+ if (!entry?.path) {
42
+ continue;
43
+ }
44
+ if (normalizedCwd === normalizePath(entry.path) && entry.name) {
45
+ return entry.name;
46
+ }
47
+ }
48
+ return fallback;
49
+ }
50
+
29
51
  function getBranch (cwd) {
30
52
  try {
31
53
  return execSync('git rev-parse --abbrev-ref HEAD', {
@@ -98,6 +120,9 @@ function loadConfig () {
98
120
  if (typeof user.webhookUrl === 'string') {
99
121
  config.webhookUrl = user.webhookUrl;
100
122
  }
123
+ if (user.listener?.projects) {
124
+ config.listenerProjects = user.listener.projects;
125
+ }
101
126
  } catch {
102
127
  // ignore malformed config
103
128
  }
@@ -720,7 +745,7 @@ process.stdin.on('end', async () => {
720
745
 
721
746
  const eventType = event.hook_event_name || 'unknown';
722
747
  const cwd = event.cwd || process.cwd();
723
- const project = path.basename(cwd);
748
+ const project = resolveProjectName(cwd, config);
724
749
  const sessionId = event.session_id || 'default';
725
750
 
726
751
  const disabled = isNotifierDisabled();
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.1.74",
4
+ "version": "1.1.76",
5
5
  "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
6
  "type": "module",
7
7
  "engines": {