claude-notification-plugin 1.1.49 → 1.1.55

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.
@@ -178,6 +178,7 @@ for (const [workDir, entry] of Object.entries(queue.queues)) {
178
178
 
179
179
  runner.on('complete', async (workDir, task, result) => {
180
180
  stopLiveConsole(workDir);
181
+ runner.cleanActivitySignal(workDir);
181
182
  const entry = queue.queues[workDir];
182
183
  const label = formatLabel(entry);
183
184
 
@@ -249,6 +250,7 @@ runner.on('complete', async (workDir, task, result) => {
249
250
 
250
251
  runner.on('error', async (workDir, task, errorMsg) => {
251
252
  stopLiveConsole(workDir);
253
+ runner.cleanActivitySignal(workDir);
252
254
  const entry = queue.queues[workDir];
253
255
  const label = formatLabel(entry);
254
256
 
@@ -268,6 +270,7 @@ runner.on('error', async (workDir, task, errorMsg) => {
268
270
 
269
271
  runner.on('timeout', async (workDir, task) => {
270
272
  stopLiveConsole(workDir);
273
+ runner.cleanActivitySignal(workDir);
271
274
  const entry = queue.queues[workDir];
272
275
  const label = formatLabel(entry);
273
276
  const timeoutMin = Math.round(taskTimeout / 60000);
@@ -296,9 +299,9 @@ function formatLabel (entry) {
296
299
  return 'unknown';
297
300
  }
298
301
  if (entry.branch && entry.branch !== 'main' && entry.branch !== 'master') {
299
- return `/${entry.project}/${entry.branch}`;
302
+ return `&${entry.project}/${entry.branch}`;
300
303
  }
301
- return `/${entry.project}`;
304
+ return `&${entry.project}`;
302
305
  }
303
306
 
304
307
  function getClaudeArgs (projectAlias) {
@@ -348,7 +351,11 @@ function startLiveConsole (workDir, messageId, header) {
348
351
  }
349
352
  lastSentText = output;
350
353
  const elapsed = formatDuration(Date.now() - new Date(runner.getActive(workDir)?.startedAt || Date.now()).getTime());
351
- const text = `${header}\n<i>${elapsed}</i>\n\n<pre>${escapeHtml(output)}</pre>`;
354
+ const activity = runner.getActivity(workDir);
355
+ const activityLine = activity && (Date.now() - activity.timestamp < 30000)
356
+ ? `\n<b>${escapeHtml(formatActivity(activity))}</b>`
357
+ : '';
358
+ const text = `${header}\n<i>${elapsed}</i>${activityLine}\n\n<pre>${escapeHtml(output)}</pre>`;
352
359
  await poller.editMessage(messageId, text);
353
360
  } catch (err) {
354
361
  logger.warn(`Live console edit error: ${err.message}`);
@@ -407,6 +414,37 @@ async function startTask (workDir, task) {
407
414
  }
408
415
  }
409
416
 
417
+ function formatActivity (activity) {
418
+ if (!activity) {
419
+ return '';
420
+ }
421
+ const { toolName, toolInput } = activity;
422
+ switch (toolName) {
423
+ case 'Edit':
424
+ case 'Write':
425
+ case 'Read':
426
+ return toolInput?.file_path ? `${toolName}: ${path.basename(toolInput.file_path)}` : toolName;
427
+ case 'Bash':
428
+ return toolInput?.command ? `$ ${toolInput.command.slice(0, 80)}` : 'Bash';
429
+ case 'Grep':
430
+ return toolInput?.pattern ? `Grep: ${toolInput.pattern}` : 'Grep';
431
+ case 'Glob':
432
+ return toolInput?.pattern ? `Glob: ${toolInput.pattern}` : 'Glob';
433
+ case 'Agent':
434
+ return toolInput?.description ? `Agent: ${toolInput.description}` : 'Agent';
435
+ case 'WebFetch':
436
+ return toolInput?.url ? `Fetch: ${toolInput.url.slice(0, 60)}` : 'WebFetch';
437
+ case 'WebSearch':
438
+ return toolInput?.query ? `Search: ${toolInput.query}` : 'WebSearch';
439
+ default:
440
+ if (toolName?.startsWith('mcp__')) {
441
+ const parts = toolName.split('__');
442
+ return parts.length >= 3 ? `MCP ${parts[1]}: ${parts[2]}` : toolName;
443
+ }
444
+ return toolName || '';
445
+ }
446
+ }
447
+
410
448
  function formatDuration (ms) {
411
449
  const sec = Math.floor(ms / 1000);
412
450
  if (sec < 60) {
@@ -448,9 +486,9 @@ async function handleCommand (cmd, args) {
448
486
  case '/stop':
449
487
  return handleStop();
450
488
  case '/help':
451
- return handleHelp();
452
489
  case '/menu':
453
- return handleMenu();
490
+ case '/start':
491
+ return handleHelp();
454
492
  default:
455
493
  return `Unknown command: ${cmd}`;
456
494
  }
@@ -469,8 +507,8 @@ function handleStatus (args) {
469
507
  for (const s of statuses) {
470
508
  const branchLabel = s.branch || 'main';
471
509
  const label = s.branch && s.branch !== 'main' && s.branch !== 'master'
472
- ? `/${target.project}/${s.branch}`
473
- : `/${target.project}`;
510
+ ? `&${target.project}/${s.branch}`
511
+ : `&${target.project}`;
474
512
  if (s.active) {
475
513
  const elapsed = s.active.startedAt
476
514
  ? formatDuration(Date.now() - new Date(s.active.startedAt).getTime())
@@ -512,8 +550,8 @@ function handleStatus (args) {
512
550
  for (const s of statuses) {
513
551
  const branchLabel = s.branch || 'main';
514
552
  const label = s.branch && s.branch !== 'main' && s.branch !== 'master'
515
- ? `/${project}/${s.branch}`
516
- : `/${project}`;
553
+ ? `&${project}/${s.branch}`
554
+ : `&${project}`;
517
555
  if (s.active) {
518
556
  const elapsed = s.active.startedAt
519
557
  ? formatDuration(Date.now() - new Date(s.active.startedAt).getTime())
@@ -550,8 +588,8 @@ function handleQueue () {
550
588
  for (const [project, statuses] of Object.entries(all)) {
551
589
  for (const s of statuses) {
552
590
  const label = s.branch && s.branch !== 'main' && s.branch !== 'master'
553
- ? `/${project}/${s.branch}`
554
- : `/${project}`;
591
+ ? `&${project}/${s.branch}`
592
+ : `&${project}`;
555
593
  if (s.active || s.queueLength > 0) {
556
594
  text += `\n<b>${escapeHtml(label)}</b>:`;
557
595
  if (s.active) {
@@ -584,12 +622,12 @@ async function handleCancel (args) {
584
622
  }
585
623
 
586
624
  if (!runner.isRunning(workDir)) {
587
- return `❌ No active task in /${escapeHtml(projectAlias)}${branch ? '/' + escapeHtml(branch) : ''}`;
625
+ return `❌ No active task in &${escapeHtml(projectAlias)}${branch ? '/' + escapeHtml(branch) : ''}`;
588
626
  }
589
627
 
590
628
  runner.cancel(workDir);
591
629
  const next = queue.cancelActive(workDir);
592
- const label = branch ? `/${projectAlias}/${branch}` : `/${projectAlias}`;
630
+ const label = branch ? `&${projectAlias}/${branch}` : `&${projectAlias}`;
593
631
 
594
632
  if (next) {
595
633
  startTask(workDir, next);
@@ -601,7 +639,7 @@ async function handleCancel (args) {
601
639
  function handleDrop (args) {
602
640
  const target = parseTarget(args);
603
641
  if (!target) {
604
- return '❌ Usage: /drop /project N';
642
+ return '❌ Usage: /drop &project N';
605
643
  }
606
644
  const index = parseInt(target.rest, 10);
607
645
  if (!index || index < 1) {
@@ -635,7 +673,7 @@ function handleClear (args) {
635
673
  }
636
674
 
637
675
  const count = queue.clearQueue(workDir);
638
- const label = branch ? `/${projectAlias}/${branch}` : `/${projectAlias}`;
676
+ const label = branch ? `&${projectAlias}/${branch}` : `&${projectAlias}`;
639
677
 
640
678
  // Also reset session
641
679
  sessions.delete(workDir);
@@ -657,7 +695,7 @@ function handleNewSession (args) {
657
695
  return `❌ ${escapeHtml(err.message)}`;
658
696
  }
659
697
 
660
- const label = branch ? `/${projectAlias}/${branch}` : `/${projectAlias}`;
698
+ const label = branch ? `&${projectAlias}/${branch}` : `&${projectAlias}`;
661
699
  const session = sessions.get(workDir);
662
700
 
663
701
  sessions.delete(workDir);
@@ -675,7 +713,7 @@ function handleProjects () {
675
713
  let text = '📂 <b>Projects:</b>\n';
676
714
  for (const [alias, proj] of Object.entries(projects)) {
677
715
  const projPath = typeof proj === 'string' ? proj : proj.path;
678
- text += `\n<b>/${escapeHtml(alias)}</b> → <code>${escapeHtml(projPath)}</code>`;
716
+ text += `\n<b>&${escapeHtml(alias)}</b> → <code>${escapeHtml(projPath)}</code>`;
679
717
  const worktrees = typeof proj === 'object' ? proj.worktrees : null;
680
718
  if (worktrees && Object.keys(worktrees).length > 0) {
681
719
  for (const [branch, wtPath] of Object.entries(worktrees)) {
@@ -689,7 +727,7 @@ function handleProjects () {
689
727
  function handleWorktrees (args) {
690
728
  const target = parseTarget(args);
691
729
  if (!target) {
692
- return '❌ Usage: /worktrees /project';
730
+ return '❌ Usage: /worktrees &project';
693
731
  }
694
732
 
695
733
  const result = worktreeManager.listWorktrees(target.project);
@@ -708,7 +746,7 @@ function handleWorktrees (args) {
708
746
  function handleCreateWorktree (args) {
709
747
  const target = parseTarget(args);
710
748
  if (!target || !target.branch) {
711
- return '❌ Usage: /worktree /project/branch';
749
+ return '❌ Usage: /worktree &project/branch';
712
750
  }
713
751
 
714
752
  const branch = target.branch;
@@ -725,7 +763,7 @@ function handleCreateWorktree (args) {
725
763
  function handleRemoveWorktree (args) {
726
764
  const target = parseTarget(args);
727
765
  if (!target || !target.branch) {
728
- return '❌ Usage: /rmworktree /project/branch';
766
+ return '❌ Usage: /rmworktree &project/branch';
729
767
  }
730
768
 
731
769
  const branch = target.branch;
@@ -740,7 +778,7 @@ function handleRemoveWorktree (args) {
740
778
  }
741
779
 
742
780
  if (workDir && runner.isRunning(workDir)) {
743
- return `❌ Cannot remove worktree: task is running. First /cancel /${escapeHtml(target.project)}/${escapeHtml(branch)}`;
781
+ return `❌ Cannot remove worktree: task is running. First /cancel &${escapeHtml(target.project)}/${escapeHtml(branch)}`;
744
782
  }
745
783
 
746
784
  try {
@@ -763,7 +801,7 @@ function handlePty (args) {
763
801
  }
764
802
  const info = runner.getSessionInfo(workDir);
765
803
  if (!info) {
766
- return `🖥 No PTY session for /${escapeHtml(target.project)}${target.branch ? '/' + escapeHtml(target.branch) : ''}`;
804
+ return `🖥 No PTY session for &${escapeHtml(target.project)}${target.branch ? '/' + escapeHtml(target.branch) : ''}`;
767
805
  }
768
806
  return formatPtyInfo(target.project, target.branch, workDir, info);
769
807
  }
@@ -786,8 +824,8 @@ function handlePty (args) {
786
824
 
787
825
  function formatPtyInfo (project, branch, workDir, info) {
788
826
  const label = branch && branch !== 'main' && branch !== 'master'
789
- ? `/${project}/${branch}`
790
- : `/${project}`;
827
+ ? `&${project}/${branch}`
828
+ : `&${project}`;
791
829
  const elapsed = info.startedAt
792
830
  ? formatDuration(Date.now() - new Date(info.startedAt).getTime())
793
831
  : '-';
@@ -816,8 +854,8 @@ function handleHistory () {
816
854
  let text = '📜 <b>Recent tasks:</b>\n';
817
855
  for (const h of history.reverse()) {
818
856
  const label = h.branch && h.branch !== 'main' && h.branch !== 'master'
819
- ? `/${h.project}/${h.branch}`
820
- : `/${h.project}`;
857
+ ? `&${h.project}/${h.branch}`
858
+ : `&${h.project}`;
821
859
  const status = h.result === 'CANCELLED' ? '🛑' : h.result?.startsWith('ERROR') ? '❌' : '✅';
822
860
  text += `\n${status} [${escapeHtml(label)}] ${escapeHtml(h.text)}`;
823
861
  }
@@ -847,34 +885,30 @@ const MENU_KEYBOARD = {
847
885
  ],
848
886
  };
849
887
 
850
- function handleMenu () {
851
- return { text: '📖 <b>Menu:</b>', replyMarkup: MENU_KEYBOARD };
852
- }
853
-
854
888
  function handleHelp () {
855
889
  return {
856
890
  text: `<b>📖 Commands:</b>
857
891
 
858
892
  /status — status of all projects
859
- /status /project — project status
893
+ /status &project — project status
860
894
  /queue — all queues
861
- /cancel [/project[/branch]] — cancel task
862
- /drop /project N — remove task from queue
863
- /clear /project[/branch] — clear queue + reset session
864
- /newsession [/project[/branch]] — reset session (keep queue)
895
+ /cancel [&project[/branch]] — cancel task
896
+ /drop &project N — remove task from queue
897
+ /clear &project[/branch] — clear queue + reset session
898
+ /newsession [&project[/branch]] — reset session (keep queue)
865
899
  /projects — list projects
866
- /worktrees /project — project worktrees
867
- /worktree /project/branch — create worktree
868
- /rmworktree /project/branch — remove worktree
869
- /pty [/project[/branch]] — PTY session diagnostics
900
+ /worktrees &project — project worktrees
901
+ /worktree &project/branch — create worktree
902
+ /rmworktree &project/branch — remove worktree
903
+ /pty [&project[/branch]] — PTY session diagnostics
870
904
  /history — task history
871
905
  /stop — stop listener
872
906
  /menu — command buttons
873
907
  /help — this help
874
908
 
875
909
  <b>Tasks:</b>
876
- <code>/project task</code> — main worktree
877
- <code>/project/branch task</code> — worktree
910
+ <code>&amp;project task</code> — main worktree
911
+ <code>&amp;project/branch task</code> — worktree
878
912
  <code>task</code> — default project
879
913
 
880
914
  <b>Session:</b>
@@ -915,7 +949,7 @@ async function handleTask (parsed, telegramMessageId) {
915
949
 
916
950
  if (autoCreated) {
917
951
  await poller.sendMessage(`🌿 Created worktree <b>${escapeHtml(parsed.branch)}</b> for "<b>${escapeHtml(parsed.project)}</b>"`);
918
- logger.info(`Auto-created worktree for task: /${parsed.project}/${parsed.branch} → ${workDir}`);
952
+ logger.info(`Auto-created worktree for task: &${parsed.project}/${parsed.branch} → ${workDir}`);
919
953
  }
920
954
 
921
955
  const result = queue.enqueue(
@@ -996,7 +1030,7 @@ async function mainLoop () {
996
1030
  }
997
1031
  }
998
1032
  } else if (parsed.type === 'task') {
999
- logger.info(`Task for /${parsed.project}${parsed.branch ? '/' + parsed.branch : ''}: ${parsed.text}`);
1033
+ logger.info(`Task for &${parsed.project}${parsed.branch ? '/' + parsed.branch : ''}: ${parsed.text}`);
1000
1034
  await handleTask(parsed, msg.messageId);
1001
1035
  }
1002
1036
  }
@@ -1,111 +1,96 @@
1
- #!/usr/bin/env node
2
-
3
- const COMMANDS = [
4
- '/status', '/queue', '/cancel', '/drop', '/clear', '/newsession',
5
- '/projects', '/worktrees', '/worktree', '/rmworktree',
6
- '/history', '/stop', '/help',
7
- ];
8
-
9
- // Telegram bot commands to silently ignore (not tasks, not our commands)
10
- const IGNORED_COMMANDS = ['/start'];
11
-
12
- /**
13
- * Parse a Telegram message into a command or task.
14
- *
15
- * Formats:
16
- * /command args → { type: 'command', cmd, args }
17
- * /project/branch text { type: 'task', project, branch, text }
18
- * /project text → { type: 'task', project, branch: null, text }
19
- * text → { type: 'task', project: 'default', branch: null, text }
20
- *
21
- * If /word is a known command, it's treated as a command.
22
- * Otherwise /word is treated as a project alias.
23
- */
24
- export function parseMessage (text) {
25
- if (!text || typeof text !== 'string') {
26
- return null;
27
- }
28
-
29
- const trimmed = text.trim();
30
- if (!trimmed) {
31
- return null;
32
- }
33
-
34
- if (trimmed.startsWith('/')) {
35
- const parts = trimmed.split(/\s+/);
36
- const cmd = parts[0].toLowerCase().replace(/@\w+$/, ''); // strip @botname
37
-
38
- // Telegram built-in commands — silently ignore
39
- if (IGNORED_COMMANDS.includes(cmd)) {
40
- return null;
41
- }
42
-
43
- // Known command
44
- if (COMMANDS.includes(cmd)) {
45
- return {
46
- type: 'command',
47
- cmd,
48
- args: parts.slice(1).join(' '),
49
- };
50
- }
51
-
52
- // Not a known command → treat as /project[/branch] task
53
- const projectMatch = trimmed.match(/^\/(\S+)\s+([\s\S]+)$/);
54
- if (projectMatch) {
55
- const target = projectMatch[1];
56
- const taskText = projectMatch[2].trim();
57
-
58
- const slashIndex = target.indexOf('/');
59
- if (slashIndex > 0) {
60
- return {
61
- type: 'task',
62
- project: target.substring(0, slashIndex),
63
- branch: target.substring(slashIndex + 1),
64
- text: taskText,
65
- };
66
- }
67
- return {
68
- type: 'task',
69
- project: target,
70
- branch: null,
71
- text: taskText,
72
- };
73
- }
74
- }
75
-
76
- // Plain text → default project
77
- return {
78
- type: 'task',
79
- project: 'default',
80
- branch: null,
81
- text: trimmed,
82
- };
83
- }
84
-
85
- /**
86
- * Parse /project or /project/branch from command args.
87
- * Returns { project, branch, rest } or null.
88
- */
89
- export function parseTarget (args) {
90
- if (!args) {
91
- return null;
92
- }
93
- const match = args.trim().match(/^\/(\S+)/);
94
- if (!match) {
95
- return null;
96
- }
97
- const target = match[1];
98
- const slashIndex = target.indexOf('/');
99
- if (slashIndex > 0) {
100
- return {
101
- project: target.substring(0, slashIndex),
102
- branch: target.substring(slashIndex + 1),
103
- rest: args.trim().substring(match[0].length).trim(),
104
- };
105
- }
106
- return {
107
- project: target,
108
- branch: null,
109
- rest: args.trim().substring(match[0].length).trim(),
110
- };
111
- }
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Parse a Telegram message into a command or task.
5
+ *
6
+ * Formats:
7
+ * /command args → { type: 'command', cmd, args }
8
+ * &project/branch text → { type: 'task', project, branch, text }
9
+ * &project text → { type: 'task', project, branch: null, text }
10
+ * text → { type: 'task', project: 'default', branch: null, text }
11
+ *
12
+ * Any /word is treated as a command (known or unknown).
13
+ * Project designation uses & prefix: &project or &project/branch.
14
+ */
15
+ export function parseMessage (text) {
16
+ if (!text || typeof text !== 'string') {
17
+ return null;
18
+ }
19
+
20
+ const trimmed = text.trim();
21
+ if (!trimmed) {
22
+ return null;
23
+ }
24
+
25
+ // Commands: anything starting with /
26
+ if (trimmed.startsWith('/')) {
27
+ const parts = trimmed.split(/\s+/);
28
+ const cmd = parts[0].toLowerCase().replace(/@\w+$/, ''); // strip @botname
29
+ return {
30
+ type: 'command',
31
+ cmd,
32
+ args: parts.slice(1).join(' '),
33
+ };
34
+ }
35
+
36
+ // Project-targeted task: &project[/branch] text
37
+ if (trimmed.startsWith('&')) {
38
+ const projectMatch = trimmed.match(/^&(\S+)\s+([\s\S]+)$/);
39
+ if (projectMatch) {
40
+ const target = projectMatch[1];
41
+ const taskText = projectMatch[2].trim();
42
+
43
+ const slashIndex = target.indexOf('/');
44
+ if (slashIndex > 0) {
45
+ return {
46
+ type: 'task',
47
+ project: target.substring(0, slashIndex),
48
+ branch: target.substring(slashIndex + 1),
49
+ text: taskText,
50
+ };
51
+ }
52
+ return {
53
+ type: 'task',
54
+ project: target,
55
+ branch: null,
56
+ text: taskText,
57
+ };
58
+ }
59
+ }
60
+
61
+ // Plain text → default project
62
+ return {
63
+ type: 'task',
64
+ project: 'default',
65
+ branch: null,
66
+ text: trimmed,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Parse &project or &project/branch from command args.
72
+ * Returns { project, branch, rest } or null.
73
+ */
74
+ export function parseTarget (args) {
75
+ if (!args) {
76
+ return null;
77
+ }
78
+ const match = args.trim().match(/^&(\S+)/);
79
+ if (!match) {
80
+ return null;
81
+ }
82
+ const target = match[1];
83
+ const slashIndex = target.indexOf('/');
84
+ if (slashIndex > 0) {
85
+ return {
86
+ project: target.substring(0, slashIndex),
87
+ branch: target.substring(slashIndex + 1),
88
+ rest: args.trim().substring(match[0].length).trim(),
89
+ };
90
+ }
91
+ return {
92
+ project: target,
93
+ branch: null,
94
+ rest: args.trim().substring(match[0].length).trim(),
95
+ };
96
+ }