@link-assistant/hive-mind 0.42.1 โ†’ 0.42.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 0.42.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 64d6cf8: Add experimental /top command to Telegram bot
8
+
9
+ - Added /top command to show live system monitor in Telegram
10
+ - Displays auto-updating `top` output in a single message (updates every 2 seconds)
11
+ - Owner-only access with chat authorization checks
12
+ - Session isolation per chat using GNU screen
13
+ - Clean stop button to terminate monitoring session
14
+ - Marked as EXPERIMENTAL feature with user warnings
15
+ - Not documented in /help as requested
16
+ - Requires GNU screen to be installed on the system
17
+
18
+ Fixes #500
19
+
20
+ ## 0.42.2
21
+
22
+ ### Patch Changes
23
+
24
+ - dca5bed: Make --auto-continue enabled by default
25
+
26
+ - Changed default value from false to true for --auto-continue in both hive and solve commands
27
+ - Smart handling of -s (--skip-issues-with-prs) flag interaction:
28
+ - When -s is used, auto-continue is automatically disabled to avoid conflicts
29
+ - Explicit --auto-continue with -s shows proper error message
30
+ - Users can still use --no-auto-continue to explicitly disable
31
+ - This improves user experience as users typically want to continue working on existing PRs
32
+
33
+ Fixes #454
34
+
3
35
  ## 0.42.1
4
36
 
5
37
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "0.42.1",
3
+ "version": "0.42.3",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
package/src/hive.mjs CHANGED
@@ -22,11 +22,9 @@ if (earlyArgs.includes('--help') || earlyArgs.includes('-h')) {
22
22
  const yargs = yargsModule.default || yargsModule;
23
23
  const { hideBin } = await use('yargs@17.7.2/helpers');
24
24
  const rawArgs = hideBin(process.argv);
25
-
26
25
  // Reuse createYargsConfig from shared module to avoid duplication
27
26
  const { createYargsConfig } = await import('./hive.config.lib.mjs');
28
27
  const helpYargs = createYargsConfig(yargs(rawArgs)).version(false);
29
-
30
28
  // Show help and exit
31
29
  helpYargs.showHelp();
32
30
  process.exit(0);
@@ -48,7 +46,6 @@ export { createYargsConfig } from './hive.config.lib.mjs';
48
46
  import { fileURLToPath } from 'url';
49
47
  const isDirectExecution = process.argv[1] === fileURLToPath(import.meta.url) ||
50
48
  (process.argv[1] && (process.argv[1].includes('/hive') || process.argv[1].endsWith('hive')));
51
-
52
49
  if (isDirectExecution) {
53
50
  console.log('๐Ÿ Hive Mind - AI-powered issue solver');
54
51
  console.log(' Initializing...');
@@ -63,7 +60,6 @@ const withTimeout = (promise, timeoutMs, operation) => {
63
60
  )
64
61
  ]);
65
62
  };
66
-
67
63
  // Use use-m to dynamically import modules for cross-runtime compatibility
68
64
  if (typeof use === 'undefined') {
69
65
  try {
@@ -157,7 +153,6 @@ async function fetchIssuesFromRepositories(owner, scope, monitorTag, fetchAllIss
157
153
  return graphqlResult.issues;
158
154
  }
159
155
  }
160
-
161
156
  // Strategy 2: Fallback to gh api --paginate approach (comprehensive but slower)
162
157
  await log(' ๐Ÿ“‹ Using gh api --paginate approach for comprehensive coverage...', { verbose: true });
163
158
 
@@ -170,18 +165,15 @@ async function fetchIssuesFromRepositories(owner, scope, monitorTag, fetchAllIss
170
165
  } else {
171
166
  repoListCmd = `gh api users/${owner}/repos --paginate --jq '.[] | {name: .name, owner: .owner.login, isArchived: .archived}'`;
172
167
  }
173
-
174
168
  await log(' ๐Ÿ“‹ Fetching repository list (using --paginate for unlimited pagination)...', { verbose: true });
175
169
  await log(` ๐Ÿ”Ž Command: ${repoListCmd}`, { verbose: true });
176
170
 
177
171
  // Add delay for rate limiting
178
172
  await new Promise(resolve => setTimeout(resolve, 2000));
179
-
180
173
  const { stdout: repoOutput } = await execAsync(repoListCmd, { encoding: 'utf8', env: process.env });
181
174
  // Parse the output line by line, as gh api with --jq outputs one JSON object per line
182
175
  const repoLines = repoOutput.trim().split('\n').filter(line => line.trim());
183
176
  const allRepositories = repoLines.map(line => JSON.parse(line));
184
-
185
177
  await log(` ๐Ÿ“Š Found ${allRepositories.length} repositories`);
186
178
 
187
179
  // Filter repositories to only include those owned by the target user/org
@@ -190,30 +182,24 @@ async function fetchIssuesFromRepositories(owner, scope, monitorTag, fetchAllIss
190
182
  return repoOwner === owner;
191
183
  });
192
184
  const unownedCount = allRepositories.length - ownedRepositories.length;
193
-
194
185
  if (unownedCount > 0) {
195
186
  await log(` โญ๏ธ Skipping ${unownedCount} repository(ies) not owned by ${owner}`);
196
187
  }
197
-
198
188
  // Filter out archived repositories from owned repositories
199
189
  const repositories = ownedRepositories.filter(repo => !repo.isArchived);
200
190
  const archivedCount = ownedRepositories.length - repositories.length;
201
-
202
191
  if (archivedCount > 0) {
203
192
  await log(` โญ๏ธ Skipping ${archivedCount} archived repository(ies)`);
204
193
  }
205
-
206
194
  await log(` โœ… Processing ${repositories.length} non-archived repositories owned by ${owner}`);
207
195
 
208
196
  let collectedIssues = [];
209
197
  let processedRepos = 0;
210
-
211
198
  // Process repositories in batches to avoid overwhelming the API
212
199
  for (const repo of repositories) {
213
200
  try {
214
201
  const repoName = repo.name;
215
202
  const ownerName = repo.owner?.login || owner;
216
-
217
203
  await log(` ๐Ÿ” Fetching issues from ${ownerName}/${repoName}...`, { verbose: true });
218
204
 
219
205
  // Build the appropriate issue list command
@@ -223,7 +209,6 @@ async function fetchIssuesFromRepositories(owner, scope, monitorTag, fetchAllIss
223
209
  } else {
224
210
  issueCmd = `gh issue list --repo ${ownerName}/${repoName} --state open --label "${monitorTag}" --json url,title,number,createdAt`;
225
211
  }
226
-
227
212
  // Add delay between repository requests
228
213
  await new Promise(resolve => setTimeout(resolve, 1000));
229
214
 
@@ -488,13 +473,26 @@ if (argv.projectMode) {
488
473
  const tool = argv.tool || 'claude';
489
474
  await validateAndExitOnInvalidModel(argv.model, tool, safeExit);
490
475
 
491
- // Validate conflicting options
492
- if (argv.skipIssuesWithPrs && argv.autoContinue) {
493
- await log('โŒ Conflicting options: --skip-issues-with-prs and --auto-continue cannot be used together', { level: 'error' });
494
- await log(' --skip-issues-with-prs: Skips issues that have any open PRs', { level: 'error' });
495
- await log(' --auto-continue: Continues with existing PRs instead of creating new ones', { level: 'error' });
496
- await log(` ๐Ÿ“ Full log file: ${absoluteLogPath}`, { level: 'error' });
497
- await safeExit(1, 'Error occurred');
476
+ // Handle -s (--skip-issues-with-prs) and --auto-continue interaction
477
+ // Detect if user explicitly passed --auto-continue or --no-auto-continue
478
+ const hasExplicitAutoContinue = rawArgs.includes('--auto-continue');
479
+ const hasExplicitNoAutoContinue = rawArgs.includes('--no-auto-continue');
480
+
481
+ if (argv.skipIssuesWithPrs) {
482
+ // If user explicitly passed --auto-continue with -s, that's a conflict
483
+ if (hasExplicitAutoContinue) {
484
+ await log('โŒ Conflicting options: --skip-issues-with-prs and --auto-continue cannot be used together', { level: 'error' });
485
+ await log(' --skip-issues-with-prs: Skips issues that have any open PRs', { level: 'error' });
486
+ await log(' --auto-continue: Continues with existing PRs instead of creating new ones', { level: 'error' });
487
+ await log(` ๐Ÿ“ Full log file: ${absoluteLogPath}`, { level: 'error' });
488
+ await safeExit(1, 'Error occurred');
489
+ }
490
+
491
+ // If user didn't explicitly set auto-continue, disable it when -s is used
492
+ // This is because -s means "skip issues with PRs" which conflicts with auto-continue
493
+ if (!hasExplicitNoAutoContinue) {
494
+ argv.autoContinue = false;
495
+ }
498
496
  }
499
497
 
500
498
  // Helper function to check GitHub permissions - moved to github.lib.mjs
@@ -751,7 +749,7 @@ async function worker(workerId) {
751
749
  const dryRunFlag = argv.dryRun ? ' --dry-run' : '';
752
750
  const skipToolConnectionCheckFlag = (argv.skipToolConnectionCheck || argv.toolConnectionCheck === false) ? ' --skip-tool-connection-check' : '';
753
751
  const toolFlag = argv.tool ? ` --tool ${argv.tool}` : '';
754
- const autoContinueFlag = argv.autoContinue ? ' --auto-continue' : '';
752
+ const autoContinueFlag = argv.autoContinue ? ' --auto-continue' : ' --no-auto-continue';
755
753
  const thinkFlag = argv.think ? ` --think ${argv.think}` : '';
756
754
  const promptPlanSubAgentFlag = argv.promptPlanSubAgent ? ' --prompt-plan-sub-agent' : '';
757
755
  const noSentryFlag = !argv.sentry ? ' --no-sentry' : '';
@@ -794,6 +792,8 @@ async function worker(workerId) {
794
792
  }
795
793
  if (argv.autoContinue) {
796
794
  args.push('--auto-continue');
795
+ } else {
796
+ args.push('--no-auto-continue');
797
797
  }
798
798
  if (argv.think) {
799
799
  args.push('--think', argv.think);
@@ -1225,45 +1225,37 @@ bot.command(/^hive$/i, async (ctx) => {
1225
1225
  }
1226
1226
  });
1227
1227
 
1228
+ // Register /top command from separate module
1229
+ // This keeps telegram-bot.mjs under the 1500 line limit
1230
+ const { registerTopCommand } = await import('./telegram-top-command.lib.mjs');
1231
+ registerTopCommand(bot, {
1232
+ VERBOSE,
1233
+ isOldMessage,
1234
+ isForwardedOrReply,
1235
+ isGroupChat,
1236
+ isChatAuthorized
1237
+ });
1238
+
1228
1239
  // Add message listener for verbose debugging
1229
- // This helps diagnose if bot is receiving messages at all
1230
1240
  if (VERBOSE) {
1231
1241
  bot.on('message', (ctx, next) => {
1232
- console.log('[VERBOSE] Message received:');
1233
- console.log('[VERBOSE] Chat ID:', ctx.chat?.id);
1234
- console.log('[VERBOSE] Chat type:', ctx.chat?.type);
1235
- console.log('[VERBOSE] Is forum:', ctx.chat?.is_forum);
1236
- console.log('[VERBOSE] Is topic message:', ctx.message?.is_topic_message);
1237
- console.log('[VERBOSE] Message thread ID:', ctx.message?.message_thread_id);
1238
- console.log('[VERBOSE] Message date:', ctx.message?.date);
1239
- console.log('[VERBOSE] Message text:', ctx.message?.text?.substring(0, 100));
1240
- console.log('[VERBOSE] From user:', ctx.from?.username || ctx.from?.id);
1241
- console.log('[VERBOSE] Bot start time:', BOT_START_TIME);
1242
- console.log('[VERBOSE] Is old message:', isOldMessage(ctx));
1243
-
1244
- // Detailed forwarding/reply detection debug info
1245
1242
  const msg = ctx.message;
1246
- const isForwarded = isForwardedOrReply(ctx);
1247
- console.log('[VERBOSE] Is forwarded/reply:', isForwarded);
1243
+ console.log('[VERBOSE] Message:', {
1244
+ chatId: ctx.chat?.id, chatType: ctx.chat?.type, isForum: ctx.chat?.is_forum,
1245
+ isTopicMsg: msg?.is_topic_message, threadId: msg?.message_thread_id, date: msg?.date,
1246
+ text: msg?.text?.substring(0, 100), user: ctx.from?.username || ctx.from?.id,
1247
+ botStartTime: BOT_START_TIME, isOld: isOldMessage(ctx), isForwarded: isForwardedOrReply(ctx),
1248
+ isAuthorized: isChatAuthorized(ctx.chat?.id)
1249
+ });
1248
1250
  if (msg) {
1249
- // Log ALL message fields to diagnose what Telegram is actually sending
1250
- console.log('[VERBOSE] Full message object keys:', Object.keys(msg));
1251
- console.log('[VERBOSE] - forward_origin:', JSON.stringify(msg.forward_origin));
1252
- console.log('[VERBOSE] - forward_origin type:', typeof msg.forward_origin);
1253
- console.log('[VERBOSE] - forward_origin truthy?:', !!msg.forward_origin);
1254
- console.log('[VERBOSE] - forward_origin.type:', msg.forward_origin?.type);
1255
- console.log('[VERBOSE] - forward_from:', JSON.stringify(msg.forward_from));
1256
- console.log('[VERBOSE] - forward_from_chat:', JSON.stringify(msg.forward_from_chat));
1257
- console.log('[VERBOSE] - forward_date:', msg.forward_date);
1258
- console.log('[VERBOSE] - reply_to_message:', JSON.stringify(msg.reply_to_message));
1259
- console.log('[VERBOSE] - reply_to_message type:', typeof msg.reply_to_message);
1260
- console.log('[VERBOSE] - reply_to_message truthy?:', !!msg.reply_to_message);
1261
- console.log('[VERBOSE] - reply_to_message.message_id:', msg.reply_to_message?.message_id);
1262
- console.log('[VERBOSE] - reply_to_message.forum_topic_created:', JSON.stringify(msg.reply_to_message?.forum_topic_created));
1251
+ console.log('[VERBOSE] Msg fields:', Object.keys(msg));
1252
+ console.log('[VERBOSE] Forward/reply:', {
1253
+ forward_origin: msg.forward_origin, forward_from: msg.forward_from,
1254
+ forward_from_chat: msg.forward_from_chat, forward_date: msg.forward_date,
1255
+ reply_to_message: msg.reply_to_message, reply_id: msg.reply_to_message?.message_id,
1256
+ forum_topic_created: msg.reply_to_message?.forum_topic_created
1257
+ });
1263
1258
  }
1264
-
1265
- console.log('[VERBOSE] Is authorized:', isChatAuthorized(ctx.chat?.id));
1266
- // Continue to next handler
1267
1259
  return next();
1268
1260
  });
1269
1261
  }
@@ -1391,9 +1383,9 @@ bot.telegram.deleteWebhook({ drop_pending_updates: true })
1391
1383
  });
1392
1384
  }
1393
1385
  return bot.launch({
1394
- // Only receive message updates (commands, text messages)
1395
- // This ensures the bot receives all message types including commands
1396
- allowedUpdates: ['message'],
1386
+ // Receive message updates (commands, text messages) and callback queries (button clicks)
1387
+ // This ensures the bot receives all message types including commands and button interactions
1388
+ allowedUpdates: ['message', 'callback_query'],
1397
1389
  // Drop any pending updates that were sent before the bot started
1398
1390
  // This ensures we only process new messages sent after this bot instance started
1399
1391
  dropPendingUpdates: true
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Telegram /top command implementation
3
+ *
4
+ * This module provides the /top command functionality for the Telegram bot,
5
+ * allowing chat owners to view live system monitor output in an auto-updating message.
6
+ *
7
+ * Features:
8
+ * - Live system monitoring using GNU screen and top command
9
+ * - Auto-updates every 2 seconds
10
+ * - Owner-only access control
11
+ * - Session management per chat
12
+ * - Clean cleanup on stop
13
+ *
14
+ * @experimental This feature is marked as experimental
15
+ */
16
+
17
+ import { promisify } from 'util';
18
+ import { exec as execCallback } from 'child_process';
19
+
20
+ const exec = promisify(execCallback);
21
+
22
+ // Store active top sessions: Map<chatId, { messageId, screenName, intervalId }>
23
+ const activeTopSessions = new Map();
24
+
25
+ /**
26
+ * Captures top output from the file for a given chat
27
+ * @param {number} chatId - The chat ID
28
+ * @returns {Promise<string|null>} The formatted top output or null on error
29
+ */
30
+ async function captureTopOutput(chatId) {
31
+ try {
32
+ const outputFile = `/tmp/top-output-${chatId}.txt`;
33
+ const { readFile } = await import('fs/promises');
34
+ const output = await readFile(outputFile, 'utf-8');
35
+
36
+ // Format output for Telegram (limit to first 30 lines to fit in message)
37
+ const lines = output.split('\n').slice(0, 30);
38
+ return lines.join('\n');
39
+ } catch (error) {
40
+ console.error('[ERROR] Failed to capture top output:', error);
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Registers the /top command handler with the bot
47
+ * @param {Object} bot - The Telegraf bot instance
48
+ * @param {Object} options - Options object
49
+ * @param {boolean} options.VERBOSE - Whether to enable verbose logging
50
+ * @param {Function} options.isOldMessage - Function to check if message is old
51
+ * @param {Function} options.isForwardedOrReply - Function to check if message is forwarded/reply
52
+ * @param {Function} options.isGroupChat - Function to check if chat is a group
53
+ * @param {Function} options.isChatAuthorized - Function to check if chat is authorized
54
+ */
55
+ export function registerTopCommand(bot, options) {
56
+ const {
57
+ VERBOSE = false,
58
+ isOldMessage,
59
+ isForwardedOrReply,
60
+ isGroupChat,
61
+ isChatAuthorized
62
+ } = options;
63
+
64
+ // /top command - show system top output in an auto-updating message (EXPERIMENTAL)
65
+ // Only accessible by chat owner
66
+ // Not documented in /help as requested in issue #500
67
+ bot.command('top', async (ctx) => {
68
+ if (VERBOSE) {
69
+ console.log('[VERBOSE] /top command received');
70
+ }
71
+
72
+ // Ignore messages sent before bot started
73
+ if (isOldMessage(ctx)) {
74
+ if (VERBOSE) {
75
+ console.log('[VERBOSE] /top ignored: old message');
76
+ }
77
+ return;
78
+ }
79
+
80
+ // Ignore forwarded or reply messages
81
+ if (isForwardedOrReply(ctx)) {
82
+ if (VERBOSE) {
83
+ console.log('[VERBOSE] /top ignored: forwarded or reply');
84
+ }
85
+ return;
86
+ }
87
+
88
+ if (!isGroupChat(ctx)) {
89
+ if (VERBOSE) {
90
+ console.log('[VERBOSE] /top ignored: not a group chat');
91
+ }
92
+ await ctx.reply('โŒ The /top command only works in group chats.', { reply_to_message_id: ctx.message.message_id });
93
+ return;
94
+ }
95
+
96
+ const chatId = ctx.chat.id;
97
+ if (!isChatAuthorized(chatId)) {
98
+ if (VERBOSE) {
99
+ console.log('[VERBOSE] /top ignored: chat not authorized');
100
+ }
101
+ await ctx.reply(`โŒ This chat (ID: ${chatId}) is not authorized to use this bot.`, { reply_to_message_id: ctx.message.message_id });
102
+ return;
103
+ }
104
+
105
+ // Check if user is chat owner
106
+ try {
107
+ const chatMember = await ctx.telegram.getChatMember(chatId, ctx.from.id);
108
+ if (chatMember.status !== 'creator') {
109
+ if (VERBOSE) {
110
+ console.log('[VERBOSE] /top ignored: user is not chat owner');
111
+ }
112
+ await ctx.reply('โŒ This command is only available to the chat owner.', { reply_to_message_id: ctx.message.message_id });
113
+ return;
114
+ }
115
+ } catch (error) {
116
+ console.error('[ERROR] Failed to check chat member status:', error);
117
+ await ctx.reply('โŒ Failed to verify permissions.', { reply_to_message_id: ctx.message.message_id });
118
+ return;
119
+ }
120
+
121
+ if (VERBOSE) {
122
+ console.log('[VERBOSE] /top passed all checks, starting...');
123
+ }
124
+
125
+ // Show experimental feature warning
126
+ await ctx.reply('๐Ÿงช *EXPERIMENTAL FEATURE*\n\nThis command is experimental and may have issues. Use with caution.', {
127
+ parse_mode: 'Markdown',
128
+ reply_to_message_id: ctx.message.message_id
129
+ });
130
+
131
+ // Check if there's already an active top session for this chat
132
+ if (activeTopSessions.has(chatId)) {
133
+ await ctx.reply('โŒ A top session is already running for this chat. Stop it first using the button.', { reply_to_message_id: ctx.message.message_id });
134
+ return;
135
+ }
136
+
137
+ // Generate screen session name with chat ID
138
+ const screenName = `top-chat-${chatId}`;
139
+
140
+ // Check if screen session already exists
141
+ let sessionExists = false;
142
+ try {
143
+ const { stdout } = await exec('screen -ls');
144
+ sessionExists = stdout.includes(screenName);
145
+ } catch {
146
+ // screen -ls returns non-zero when no sessions exist
147
+ sessionExists = false;
148
+ }
149
+
150
+ // Create screen session if it doesn't exist
151
+ // We'll use a different approach: run top in batch mode with output redirected to a file
152
+ // that we continuously read instead of using screen hardcopy
153
+ const outputFile = `/tmp/top-output-${chatId}.txt`;
154
+
155
+ if (!sessionExists) {
156
+ try {
157
+ // Start top in a screen session with batch mode, outputting to a file
158
+ // -b: batch mode, -d 2: 2 second delay between updates, -n: number of iterations (unlimited)
159
+ await exec(`screen -dmS ${screenName} bash -c 'while true; do top -b -n 1 > ${outputFile}; sleep 2; done'`);
160
+ if (VERBOSE) {
161
+ console.log(`[VERBOSE] Created screen session: ${screenName}`);
162
+ }
163
+ // Give top a moment to start and produce first output
164
+ await new Promise(resolve => setTimeout(resolve, 1500));
165
+ } catch (error) {
166
+ console.error('[ERROR] Failed to create screen session:', error);
167
+ await ctx.reply('โŒ Failed to start top command.', { reply_to_message_id: ctx.message.message_id });
168
+ return;
169
+ }
170
+ }
171
+
172
+ // Send initial message with loading indicator
173
+ const initialMessage = await ctx.reply('๐Ÿงช ๐Ÿ“Š Loading system monitor... (EXPERIMENTAL)', {
174
+ reply_to_message_id: ctx.message.message_id,
175
+ reply_markup: {
176
+ inline_keyboard: [[
177
+ { text: '๐Ÿ›‘ Stop', callback_data: `stop_top_${chatId}` }
178
+ ]]
179
+ }
180
+ });
181
+
182
+ // Capture and display first output
183
+ const firstOutput = await captureTopOutput(chatId);
184
+ if (firstOutput) {
185
+ try {
186
+ await ctx.telegram.editMessageText(
187
+ chatId,
188
+ initialMessage.message_id,
189
+ undefined,
190
+ `\`\`\`\n${firstOutput}\n\`\`\``,
191
+ {
192
+ parse_mode: 'Markdown',
193
+ reply_markup: {
194
+ inline_keyboard: [[
195
+ { text: '๐Ÿ›‘ Stop', callback_data: `stop_top_${chatId}` }
196
+ ]]
197
+ }
198
+ }
199
+ );
200
+ } catch (error) {
201
+ console.error('[ERROR] Failed to update message:', error);
202
+ }
203
+ }
204
+
205
+ // Set up periodic update (every 2 seconds)
206
+ const intervalId = setInterval(async () => {
207
+ const output = await captureTopOutput(chatId);
208
+ if (output) {
209
+ try {
210
+ await ctx.telegram.editMessageText(
211
+ chatId,
212
+ initialMessage.message_id,
213
+ undefined,
214
+ `\`\`\`\n${output}\n\`\`\``,
215
+ {
216
+ parse_mode: 'Markdown',
217
+ reply_markup: {
218
+ inline_keyboard: [[
219
+ { text: '๐Ÿ›‘ Stop', callback_data: `stop_top_${chatId}` }
220
+ ]]
221
+ }
222
+ }
223
+ );
224
+ } catch (error) {
225
+ // Ignore "message is not modified" errors
226
+ if (!error.message?.includes('message is not modified')) {
227
+ console.error('[ERROR] Failed to update message:', error);
228
+ }
229
+ }
230
+ }
231
+ }, 2000);
232
+
233
+ // Store session info
234
+ activeTopSessions.set(chatId, {
235
+ messageId: initialMessage.message_id,
236
+ screenName,
237
+ intervalId
238
+ });
239
+
240
+ if (VERBOSE) {
241
+ console.log(`[VERBOSE] Top session started for chat ${chatId}`);
242
+ }
243
+ });
244
+
245
+ // Handle stop button callback
246
+ bot.action(/^stop_top_(.+)$/, async (ctx) => {
247
+ const chatId = parseInt(ctx.match[1]);
248
+
249
+ if (VERBOSE) {
250
+ console.log(`[VERBOSE] Stop top callback received for chat ${chatId}`);
251
+ }
252
+
253
+ // Check if user is chat owner
254
+ try {
255
+ const chatMember = await ctx.telegram.getChatMember(chatId, ctx.from.id);
256
+ if (chatMember.status !== 'creator') {
257
+ await ctx.answerCbQuery('โŒ Only the chat owner can stop the top session.');
258
+ return;
259
+ }
260
+ } catch (error) {
261
+ console.error('[ERROR] Failed to check chat member status:', error);
262
+ await ctx.answerCbQuery('โŒ Failed to verify permissions.');
263
+ return;
264
+ }
265
+
266
+ const session = activeTopSessions.get(chatId);
267
+ if (!session) {
268
+ await ctx.answerCbQuery('โŒ No active top session found.');
269
+ return;
270
+ }
271
+
272
+ // Stop the update interval
273
+ clearInterval(session.intervalId);
274
+
275
+ // Kill the screen session
276
+ try {
277
+ await exec(`screen -S ${session.screenName} -X quit`);
278
+ if (VERBOSE) {
279
+ console.log(`[VERBOSE] Killed screen session: ${session.screenName}`);
280
+ }
281
+ } catch (error) {
282
+ console.error('[ERROR] Failed to kill screen session:', error);
283
+ }
284
+
285
+ // Clean up the output file
286
+ try {
287
+ const { unlink } = await import('fs/promises');
288
+ await unlink(`/tmp/top-output-${chatId}.txt`);
289
+ if (VERBOSE) {
290
+ console.log(`[VERBOSE] Cleaned up output file for chat ${chatId}`);
291
+ }
292
+ } catch (error) {
293
+ // Ignore file cleanup errors
294
+ if (VERBOSE) {
295
+ console.log(`[VERBOSE] Could not clean up output file: ${error.message}`);
296
+ }
297
+ }
298
+
299
+ // Remove from active sessions
300
+ activeTopSessions.delete(chatId);
301
+
302
+ // Update the message to show it's stopped
303
+ try {
304
+ await ctx.editMessageText('๐Ÿ›‘ Top session stopped.', {
305
+ parse_mode: 'Markdown'
306
+ });
307
+ } catch (error) {
308
+ console.error('[ERROR] Failed to edit message:', error);
309
+ }
310
+
311
+ await ctx.answerCbQuery('โœ… Top session stopped successfully.');
312
+
313
+ if (VERBOSE) {
314
+ console.log(`[VERBOSE] Top session stopped for chat ${chatId}`);
315
+ }
316
+ });
317
+ }
318
+
319
+ /**
320
+ * Gets information about active top sessions
321
+ * @returns {Map} Map of active sessions
322
+ */
323
+ export function getActiveTopSessions() {
324
+ return activeTopSessions;
325
+ }