claude-notification-plugin 1.1.52 → 1.1.56
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/README.md +38 -34
- package/commit-sha +1 -1
- package/hooks/hooks.json +89 -34
- package/listener/LISTENER-DETAILED.md +117 -94
- package/listener/listener.js +84 -50
- package/listener/message-parser.js +96 -111
- package/listener/pty-runner.js +122 -98
- package/listener/telegram-poller.js +21 -4
- package/notifier/NOTIFIER-DETAILED.md +82 -0
- package/notifier/notifier.js +88 -19
- package/package.json +1 -1
package/listener/listener.js
CHANGED
|
@@ -127,8 +127,8 @@ const runner = new PtyRunner(logger, taskTimeout, taskLogger, taskLogDir);
|
|
|
127
127
|
const worktreeManager = new WorktreeManager(config, logger);
|
|
128
128
|
|
|
129
129
|
const liveConsoleEnabled = listenerConfig.liveConsole !== false; // default: true
|
|
130
|
-
const
|
|
131
|
-
const
|
|
130
|
+
const liveConsoleIntervalMillis = (listenerConfig.liveConsoleIntervalMillis || 1) * 1000;
|
|
131
|
+
const liveConsoleMaxOutputChars = listenerConfig.liveConsoleMaxOutputChars || 300;
|
|
132
132
|
|
|
133
133
|
const startTime = Date.now();
|
|
134
134
|
|
|
@@ -142,7 +142,7 @@ const liveConsoleTimers = new Map();
|
|
|
142
142
|
logger.info('Listener started');
|
|
143
143
|
logger.info(`Projects: ${JSON.stringify(Object.keys(listenerConfig.projects))}`);
|
|
144
144
|
logger.info(`Session continuity: ${continueSessionEnabled ? 'enabled' : 'disabled'}`);
|
|
145
|
-
logger.info(`Live console: ${liveConsoleEnabled ? `enabled (${
|
|
145
|
+
logger.info(`Live console: ${liveConsoleEnabled ? `enabled (${liveConsoleIntervalMillis / 1000}s interval, max ${liveConsoleMaxOutputChars} chars)` : 'disabled'}`);
|
|
146
146
|
|
|
147
147
|
// ----------------------
|
|
148
148
|
// DISCOVER WORKTREES ON START
|
|
@@ -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
|
|
302
|
+
return `&${entry.project}/${entry.branch}`;
|
|
300
303
|
}
|
|
301
|
-
return
|
|
304
|
+
return `&${entry.project}`;
|
|
302
305
|
}
|
|
303
306
|
|
|
304
307
|
function getClaudeArgs (projectAlias) {
|
|
@@ -336,11 +339,11 @@ function startLiveConsole (workDir, messageId, header) {
|
|
|
336
339
|
return;
|
|
337
340
|
}
|
|
338
341
|
// Take the tail that fits
|
|
339
|
-
const tail = cleaned.length >
|
|
340
|
-
? cleaned.slice(-
|
|
342
|
+
const tail = cleaned.length > liveConsoleMaxOutputChars
|
|
343
|
+
? cleaned.slice(-liveConsoleMaxOutputChars)
|
|
341
344
|
: cleaned;
|
|
342
345
|
// Trim to last complete line if we sliced mid-line
|
|
343
|
-
const output = cleaned.length >
|
|
346
|
+
const output = cleaned.length > liveConsoleMaxOutputChars
|
|
344
347
|
? tail.slice(tail.indexOf('\n') + 1)
|
|
345
348
|
: tail;
|
|
346
349
|
if (!output || output === lastSentText) {
|
|
@@ -348,12 +351,16 @@ 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
|
|
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}`);
|
|
355
362
|
}
|
|
356
|
-
},
|
|
363
|
+
}, liveConsoleIntervalMillis);
|
|
357
364
|
liveConsoleTimers.set(workDir, timer);
|
|
358
365
|
}
|
|
359
366
|
|
|
@@ -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
|
-
|
|
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
|
-
?
|
|
473
|
-
:
|
|
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
|
-
?
|
|
516
|
-
:
|
|
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
|
-
?
|
|
554
|
-
:
|
|
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
|
|
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 ?
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
?
|
|
790
|
-
:
|
|
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
|
-
?
|
|
820
|
-
:
|
|
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
|
|
893
|
+
/status &project — project status
|
|
860
894
|
/queue — all queues
|
|
861
|
-
/cancel [
|
|
862
|
-
/drop
|
|
863
|
-
/clear
|
|
864
|
-
/newsession [
|
|
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
|
|
867
|
-
/worktree
|
|
868
|
-
/rmworktree
|
|
869
|
-
/pty [
|
|
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
|
|
877
|
-
<code
|
|
910
|
+
<code>&project task</code> — main worktree
|
|
911
|
+
<code>&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:
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}
|